typescript 105 lines · 3 tabs

Group Chat Messages by Day with a Memoized Selector in React

Shared by codesnips Sep 2026
3 tabs
import { startOfDay, isToday, isYesterday, format } from 'date-fns';

export interface ChatMessage {
  id: string;
  senderId: string;
  senderName: string;
  body: string;
  sentAt: string; // ISO timestamp
}

export interface GroupedMessage extends ChatMessage {
  senderChange: boolean;
}

export interface MessageGroup {
  dayKey: string;
  label: string;
  messages: GroupedMessage[];
}

function formatDayLabel(date: Date): string {
  if (isToday(date)) return 'Today';
  if (isYesterday(date)) return 'Yesterday';
  return format(date, 'EEEE, MMMM d');
}

export function groupMessagesByDay(messages: ChatMessage[]): MessageGroup[] {
  const groups: MessageGroup[] = [];

  for (const message of messages) {
    const day = startOfDay(new Date(message.sentAt));
    const dayKey = day.toISOString();
    let current = groups[groups.length - 1];

    if (!current || current.dayKey !== dayKey) {
      current = { dayKey, label: formatDayLabel(day), messages: [] };
      groups.push(current);
    }

    const prev = current.messages[current.messages.length - 1];
    const senderChange = !prev || prev.senderId !== message.senderId;
    current.messages.push({ ...message, senderChange });
  }

  return groups;
}
3 files · typescript Explain with highlit

This snippet shows how a chat UI turns a flat, chronologically ordered array of messages into a day-grouped structure that renders as dated sections. The grouping logic is kept out of the components entirely and lives in a pure selector, which is the key idea: view components stay dumb and cheap, while the potentially expensive transformation is computed once and reused.

In groupMessagesByDay selector, the raw ChatMessage[] is folded into an ordered array of MessageGroup objects. Each message is bucketed by a stable dayKey derived from startOfDay, and formatDayLabel renders human labels like Today and Yesterday using date-fns helpers rather than raw dates. Because the input is already sorted, the reducer can append to the current group when the key matches and otherwise open a new group, giving O(n) grouping with no re-sorting. The selector also carries a senderChange flag per message so the view can collapse consecutive messages from the same author into a visual cluster.

The useGroupedMessages hook wraps the selector in useMemo, keyed on the messages reference. This matters because grouping runs on every render otherwise; memoizing means it only recomputes when the message list actually changes identity. This is why upstream state should replace the array immutably rather than mutate it in place, or the memo will go stale.

In MessageList component, the hook output drives rendering. Each MessageGroup becomes a section with a sticky DayDivider, and messages map to MessageBubble, which uses senderChange and isOwn to decide whether to show the avatar and author name. Keeping formatting concerns (formatTime) at the leaf keeps the selector focused purely on structure.

The trade-off is that the selector recomputes the entire structure when any message changes; for very large histories a windowed or incremental approach would be better, but for typical chat pane sizes this pattern is simple, testable, and fast. It is the go-to approach when a component needs derived, grouped data without coupling that logic to rendering.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
python
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
from sklearn.linear_model import LogisticRegression

standard_pipeline = Pipeline([
    ('scaler', StandardScaler()),

Scaling and normalization choices for different model families

feature-scaling normalization machine-learning
by Dr. Elena Vasquez 1 tab
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
ruby
module EmailNormalization
  extend ActiveSupport::Concern

  included do
    attr_accessor :soft_warnings

Soft Validation: Normalize + Validate Email

rails activerecord validations
by codesnips 4 tabs
typescript
import React from "react";

type FallbackProps = {
  error: Error;
  reset: () => void;
};

React Error Boundary + error reporting hook

react frontend error-boundary
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Group Chat Messages by Day with a Memoized Selector in React — share card
Link copied