This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Repository: Stream's React Chat SDK - 40+ React components for building chat UIs with the Stream Chat API.
Key Files:
AI.md- Integration patterns for usersAGENTS.md- Repository structure & contribution workflowdevelopers/- Detailed development guides
# Development (requires Node 24 β see .nvmrc)
yarn install # Setup
yarn build # Full build (translations, Vite, types, SCSS)
yarn test # Run Jest tests
yarn test <pattern> # Run specific test (e.g., yarn test Channel)
yarn lint-fix # Fix all lint/format issues (prettier + eslint)
yarn types # TypeScript type checking (noEmit mode)
# E2E
yarn e2e-fixtures # Generate e2e test fixtures
yarn e2e # Run Playwright tests
# Before committing
yarn lint-fix # ALWAYS run this first<Chat> # Root: provides client, theme, i18n
ββ <Channel> # State container: messages, threads, WebSocket events
ββ <MessageList> # Renders messages (or <VirtualizedMessageList>)
ββ <MessageInput> # Composer with attachments/mentions
ββ <Thread> # Threaded replies
ChatContext # Client, active channel, theme, navigation
ββ ChannelStateContext # Read-only: messages, members, loading states
ββ ChannelActionContext # Write: sendMessage, deleteMessage, openThread
ββ ComponentContext # 100+ customizable component slots
ββ MessageContext # Per-message: actions, reactions, status
All contexts have hooks: useChatContext(), useChannelStateContext(), etc.
- Local state (
useState) - Component UI state - Reducer state (
useReducer) - Channel usesmakeChannelReducerfor complex message state - Context state - Global shared state
- External state -
stream-chatSDK's StateStore viauseStateStorehook (usesuseSyncExternalStore)
File: src/components/Channel/Channel.tsx + channelState.ts
- Messages are added to local state IMMEDIATELY when sending (optimistic)
- WebSocket events may arrive before/after API response
- Timestamp-based conflict resolution: Newest version always wins
- Gotcha: Thread state is separate from channel state - both must be updated
File: src/components/Channel/Channel.tsx (handleEvent function)
// Events are THROTTLED to 500ms to prevent excessive re-renders
throttledCopyStateFromChannel = throttle(
() => dispatch({ type: 'copyStateFromChannelOnEvent' }),
500,
{ leading: true, trailing: true },
);Key behaviors:
- Some events ignored:
user.watching.start/stop - Unread updates throttled separately (200ms)
- Message filtering:
parent_id+show_in_channeldetermine thread visibility
File: src/components/MessageList/utils.ts
Messages are processed in order:
- Date separator insertion (by date comparison)
- Unread separator (only for other users' messages)
- Deleted messages filtered/kept based on config
- Giphy preview extraction (for VirtualizedMessageList)
- Group styling applied (user ID + time gaps)
Gotcha: If hideDeletedMessages=true, date separators still needed when next message has different date.
Files: src/components/MessageList/VirtualizedMessageList.tsx + VirtualizedMessageListComponents.tsx
- Uses react-virtuoso with custom item sizing
- Offset trick:
PREPEND_OFFSET = 10^7inVirtualizedMessageListComponents.tsxhandles prepended messages without Virtuoso knowing - Only visible items + overscan buffer rendered
skipMessageDataMemoizationprop exists for channels with 1000s of messages
Critical memoization:
- Message data serialized to string for comparison (see
useCreateChannelStateContext) areMessageUIPropsEqualchecks cheap props first (highlighted, mutes.length)- Gotcha: Any prop not in serialization won't trigger updates!
Throttling locations:
- WebSocket events: 500ms
- Unread counter updates: 200ms
markRead: 500ms (leading: true, trailing: false - only fires on FIRST call)loadMoreFinished: 2000ms debounced
- Mutate
channel.state.messagesdirectly - Usechannel.state.addMessageSorted()/removeMessage() - Include
channelin dependency arrays - Usechannel.cidonly (stable), notchannel.state(changes constantly) - Modify reducer action types without updating all dispatchers - They're tightly coupled
- Change message sort order - SDK maintains order; local changes will conflict
- Forget to update both channel AND thread state - Thread messages must exist in main state too
- Main channel:
state.messages(flat list) - Threads:
state.threads[parentId](keyed by parent message ID) - Invariant: Messages in threads MUST also exist in main channel state
useMemo(
() => ({
/* value */
}),
[
channel.cid, // β
Stable - include this
deleteMessage, // β
Stable callback
// β NOT channel.state.messages - causes infinite re-renders
// β NOT channel.initialized - changes constantly
],
);File: src/mock-builders/
// Standard test setup
const chatClient = await getTestClientWithUser({ id: 'test-user' });
useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannelData)]);
const channel = chatClient.channel('messaging', channelId);
await channel.watch();Key mocks:
client.connectionId = 'dummy_connection_id'client.wsPromise = Promise.resolve(true)(mocks WebSocket)- Mock methods on channel, not entire channel object
render(
<Chat client={chatClient}>
<Channel channel={channel}>
<MessageList />
</Channel>
</Chat>,
);Tightest Coupling:
Message.tsxβMessageContext- Every message needs actionsChannel.tsxβVirtualizedMessageList- Complex prop drillinguseCreateChannelStateContextβ Message memoization - String serialization fragility
Integration Risks:
- Modifying reducer actions requires updates in multiple dispatchers
- Changing message sorting conflicts with SDK updates
- Thread state isolation is error-prone
Component structure:
ComponentName/
βββ ComponentName.tsx
βββ hooks/ # Component-specific hooks
βββ styling/ # SCSS files
βββ utils/ # Component utilities
βββ __tests__/ # Tests
βββ index.ts
Hook organization: Component-specific hooks in hooks/ subdirectories:
Channel/hooks/- Channel state, typing, editingMessage/hooks/- Actions (delete, pin, flag, react, retry)MessageInput/hooks/- Input controls, attachments, submissionMessageList/hooks/- Scroll, enrichment, notifications
Commit format: Conventional Commits (enforced by commitlint)
feat(MessageInput): add audio recording support
Implement MediaRecorder API integration with MP3 encoding.
Closes #123
PR Requirements:
-
yarn lint-fixpassed -
yarn testpassed -
yarn typespassed - Tests added for changes
- No new warnings (zero tolerance)
- Screenshots for UI changes
Release: Automated via semantic-release based on commit messages.
When deprecating, use @deprecated JSDoc tag with reason and docs link. Commit under deprecate type. See developers/DEPRECATIONS.md for full process.
The build runs 4 steps in parallel via concurrently:
build-translationsβ Extractst()calls from source viai18next-clivite buildβ Bundles 3 entry points (index, emojis, mp3-encoder) as CJS + ESM, no minificationtscβ Generates.d.tstype declarations only (tsconfig.lib.json) todist/types/build-stylingβ Compilessrc/styling/index.scssβdist/css/index.css
All steps write to separate directories under dist/ so they don't conflict.
All component styles live in src/styling/ (master entry: src/styling/index.scss) and in src/components/*/styling/index.scss. The Sass build compiles the tree to dist/css/index.css. There is no longer any step that pulls CSS/SCSS from an external design-system package.
css-reset β stream-new (compiled index.css) β stream-overrides β stream-app-overrides
See examples/vite/src/index.scss for reference implementation. Layers eliminate the need for !important.
- Primitives (
src/styling/variables.css) β Figma-sourced:--slate-50,--blue-500, etc. - Semantic tokens (
src/styling/_global-theme-variables.scss) β--str-chat__primary-color,--str-chat__text-colorwith light/dark variants - Component tokens (per-component SCSS) β
--str-chat__message-bubble-background-color, etc.
- 12 languages: de, en, es, fr, hi, it, ja, ko, nl, pt, ru, tr (JSON files in
src/i18n/) - Keys are English text:
t('Mute'),t('{{ user }} is typing...') - Extraction:
i18next-cli extractscanst()calls in source β updates JSON files - Validation:
yarn lintrunsscripts/validate-translations.jsβ fails on any empty translation string (zero tolerance) - Date/time:
Streami18nclass wraps i18next + Dayjs with per-locale calendar formats - When adding translatable strings: Use
t()fromuseTranslationContext(), then runyarn build-translationsto update JSON files. All 12 language files must have non-empty values.
All styles live in src/styling/ (master entry: src/styling/index.scss) and in src/components/*/styling/index.scss. Component styles are imported by the master stylesheet and compiled to dist/css/index.css via Sass.
CSS layers control cascade order (no !important needed):
css-reset β stream-new (compiled SDK CSS) β stream-overrides β stream-app-overrides
See examples/vite/src/index.scss for the reference layer setup.
Theming uses a 3-tier CSS variable hierarchy:
- Primitives (
src/styling/variables.css) β Figma-sourced color palette tokens - Semantic tokens (
src/styling/_global-theme-variables.scss) β Light/dark mode mappings (e.g.,--str-chat__primary-color) - Component tokens (per-component SCSS) β e.g.,
--str-chat__message-bubble-background-color
yarn build runs 4 tasks in parallel via concurrently:
yarn build-translationsβ Extractst()calls viai18next-clivite buildβ Bundles 3 entry points (index, emojis, mp3-encoder) as ESM + CJStsc --project tsconfig.lib.jsonβ Generates.d.tstype declarations todist/types/yarn build-stylingβ Compiles SCSS todist/css/index.css
Library entry points (from package.json exports):
stream-chat-reactβ Main SDK (all components, hooks, contexts)stream-chat-react/emojisβ Emoji picker plugin (src/plugins/Emojis/)stream-chat-react/mp3-encoderβ MP3 encoding for voice messages (src/plugins/encoders/mp3.ts)
Vite config: no minification, sourcemaps enabled, all deps externalized. Target: ES2020.
- 12 languages in
src/i18n/*.jsonβ Natural language keys (English text = key) yarn build-translationsextractst()calls from source viai18next-cli extractyarn validate-translations(runs duringyarn lint) β zero-tolerance: any empty string value fails the buildStreami18nclass (src/i18n/Streami18n.ts) wraps i18next, integrates Dayjs for date/time formatting- Interpolation:
t('Failed to update {{ field }}', { field }), Plurals:_one/_othersuffixes - Access via
useTranslationContext()hook β only works inside<Chat>
- Add to
ComponentContext(src/context/ComponentContext.tsx) - Provide default implementation
- Allow override via prop:
<Channel Message={CustomMessage} /> - Access via
useComponentContext()
import { useStateStore } from './store';
const channels = useStateStore(chatClient.state.channelsArray);- Add strings to
src/i18n/ - Run
yarn build-translations - Use:
const { t } = useTranslationContext();
- Integration patterns: See
AI.md - Repo structure: See
AGENTS.md - Development guides: See
developers/ - Component docs: https://getstream.io/chat/docs/sdk/react/
- Stream Chat API: https://getstream.io/chat/docs/javascript/