How the SDK is built
The SDK has two layers. This page follows one action through both of them: a user opens a channel and sends a message. It ends with the rule that follows from it: which layer to change for what.
The two layers
- State lives in
stream-chat. Channels, threads, channel lists, message lists and message composers are objects, and each keeps its data in stores that anything can subscribe to. You configure these objects throughclient.config. None of this depends on React. - UI lives in
stream-chat-react-native, orstream-chat-expoin Expo apps. Its components subscribe to those stores withuseStateStore, render what they hold, and call methods on the objects when the user acts. You replace any component throughWithComponents.
Where a channel or a thread opens is up to your app's navigation. The SDK calls back when the user picks a channel (onSelect on ChannelList) or opens a thread (onThreadSelect on MessageList and ThreadList), and your screens render Channel and Thread.
One action through the layers
The user has the app from the Quick start open: a channel list screen and a channel screen. They open "Design" and send "Hello". Step through what happens in each layer:
flowchart LR
subgraph UI["UI: stream-chat-react-native"]
CL[ChannelList]
ML[MessageList]
MC[MessageComposer]
end
subgraph State["State: stream-chat"]
CP[ChannelPaginator]
MP[channel.messagePaginator]
CO[channel.messageComposer]
MO[channel.messageOperations]
end
API[(Stream API)]
CP -- channels --> CL
MP -- messages --> ML
CO -- what is typed --> MC
MC -- "send()" --> CO
CO --> MO
MO -- optimistic message --> MP
MO -- request --> API
API -- "response, message.new" --> MP
API -- "message.new" --> CPChannelPaginator loads the channels from the Stream API with a channel query, and feeds them to ChannelList: the list subscribes to paginator.state and renders a row for each channel. The paginator is registered on client.channelManager, which keeps it up to date as events arrive.
Tapping "Design" calls the list's onSelect with the channel, and the app navigates to its channel screen, which renders Channel for it. Channel watches the channel unless it's already watched and loaded with its latest messages, activates it, and marks it read if it has unread messages. Inside it, MessageList subscribes to channel.messagePaginator and renders its messages, and MessageComposer renders from channel.messageComposer.
Typing in MessageComposer writes the text into channel.messageComposer, and the composer UI renders the composer's state back.
Pressing the send button calls MessageComposer.send(), which hands the message to channel.messageOperations. Two paths follow. The local one adds the message to messagePaginator at once, marked as sending, so MessageList shows it right away. The other sends the request to the Stream API, through channel.requestHandlers.sendMessageRequest in client.config if you set one. The composer is cleared.
The API answers with its copy of the message. messageOperations stores it in messagePaginator in place of the local one, marked as received, and MessageList re-renders it as sent. If the request fails, the message is marked failed instead, and the user can retry it from the message menu. With offline support, a text message sent while the device is offline is kept in the local database and sent again when the connection comes back.
The API also sends message.new to every client watching the channel, this one included. The channel ingests the message into messagePaginator, where it's already present, so nothing changes there, and your own message doesn't count as unread. The channel manager re-inserts "Design" into its paginators, which sort by last message, so it moves to the top and ChannelList re-renders in that order.
The UI rendered things and the app's navigation placed them; the state layer loaded the data, sent the message and updated the stores. In a thread, the same steps run on thread.messagePaginator, thread.messageComposer and thread.messageOperations, and the send still goes through the channel's request handler.
The rule
Change behavior in the state layer. Change appearance in the UI layer.
- Behavior is what the objects do: what they load, what they send and what they allow. Change it through
client.config: configuration values, request handlers, and setup functions that add composer middleware. The change applies wherever the object is used: in the SDK's components, in your own components, and in channels and threads alike. - Appearance is what a component renders. Replace the component through
WithComponents, or change the theme. - Placement is where a channel or a thread opens. It belongs to your app's navigation: the screens that render
ChannelandThread, and the callbacks that open them.
Two examples:
- Turning on drafts is behavior: set
messageComposer.drafts.enabledinclient.config. It reaches every message composer, in channels and threads, including a composer UI of your own, because it configures the composer objects, not a component. - Changing the date separator is appearance: pass an
InlineDateSeparatoroverride toWithComponents. Every message list below it renders yours.
Where each layer is taught
- State. The State section explains how the objects hold data, load it, keep it current and change it. Start with Reactive state. Channel list paginators cover channel lists, and Message composer and Message operations cover writing and sending. Configuration covers
client.config, and Offline support covers the local database. - UI. The UI section explains how components read state and how to change any of them.