Configuration
The client builds most objects for you: channels, threads, composers, message lists, and its own managers for the connection, notifications and reminders. Each of them has settings, for example whether a composer saves drafts, how many messages a list loads per page, or how long the connection waits before it retries. This page goes through them in order: where settings come from, how your app sets them, how they combine, and how to read the result.
Client and server settings
An object's settings come from two places:
- Client settings are what your app sets in code. They decide how the SDK's objects behave, for example how many messages a list loads per page, whether composers save drafts, or how a message is sent.
- Server settings live on the server, and your app only reads them. They say what the channel type allows, what the current user may do, whether slow mode is on and what the user's privacy settings are. See Server settings.
Some features need both. Your app turns a feature on, and the server can still turn it off. Who decides what lists them. The next section is about client settings.
Client settings
The settings tree
The SDK's objects come in kinds: channels, threads, composers, message lists, and so on. Client settings are set per kind, and a setting applies to every object of its kind. They form a tree, with one key for each kind:
| Key | Configures |
|---|---|
channel |
every channel: typing, read and delivery events, replies, reminders, request handlers, its message list and its pinned-messages list |
thread |
every thread: its request handlers and its reply list |
messageComposer |
every composer, in channels, threads and edits: text, attachments, drafts, link previews, polls, location, commands |
messagePaginator |
every message list, a channel's and a thread's alike. channel.messagePaginator and thread.messagePaginator override it field by field |
messageOperations |
how every channel and thread sends, edits and deletes messages and reactions. Its settings say how many failed sends it keeps for retry, and for how long. channel.messageOperations and thread.messageOperations override it |
client |
the client and its managers: the connection, connection recovery, delivery receipts, notifications, reminders and the thread list |
liveLocationManager |
live location sharing |
searchController |
search |
Every setting is listed in Configuration tree.
The keys are fixed. You can't add one, not even with module augmentation. To make a class of your own configurable, see Making your own class configurable.
When you create the client
Pass the tree when you create the client. Say every composer should save drafts, and every channel's message list should load 50 messages per page. With useCreateChatClient, pass the tree as options:
import { useCreateChatClient } from "stream-chat-react-native";
export const useChatClient = () =>
useCreateChatClient({
apiKey: "YOUR_API_KEY",
options: {
channel: { messagePaginator: { pageSize: 50 } },
messageComposer: { drafts: { enabled: true } },
},
tokenOrProvider: "YOUR_USER_TOKEN",
userData: { id: "YOUR_USER_ID" },
});The hook reads options once, on the first render, and passes them to the StreamChat constructor. A client you create yourself takes the same tree as its second argument:
import { StreamChat } from "stream-chat";
const chatClient = new StreamChat("YOUR_API_KEY", {
channel: { messagePaginator: { pageSize: 50 } },
messageComposer: { drafts: { enabled: true } },
});StreamChat.getInstance(apiKey, config) takes the same tree the first time it's called, and ignores it after that.
A few settings are read only while the client is being built, so they have to go here. Settings read once lists them.
After the client is created
The client keeps the tree in client.config, and you can change it at any time. A change also reaches the objects that exist already, except for the few settings read once. It can also undo what you changed on one object with updateConfig(), as How the settings combine explains.
For example, to turn drafts off again:
client.config.set({
messageComposer: { drafts: { enabled: false } },
});client.config has these methods:
client.config.set(tree)merges into what's already set, so a later call changes only the paths it names.client.config.setConfig(key, subtree)does the same for one key, such as"messageComposer".client.config.getConfig(key)returns what you set for one key, andclient.config.getTree()the whole tree.client.config.reset(key?)clearsclient.config, for one key or for every key when you pass none. That includes the tree you passed when you created the client, the settings the SDK's components registered (see What the React Native SDK sets), and the setup function, whose teardown runs. Every existing object then rebuilds its settings right away. It goes back to the SDK's defaults and drops itsupdateConfig()changes. It keeps the options your code passed when it created the object, such as aChannelPaginator'spaginatorOptions, and the server's limits still apply. Only the settings read once have no effect on existing objects, because those objects already used them when they were created.
Settings for one object
The ways above apply to every object of a kind. Two ways change a single object:
- Its constructor, for an object your code creates. A
ChannelPaginator, for example, takes its settings when you create it, as Channel list paginators shows. updateConfig(patch), which these objects have:
| Call | Changes |
|---|---|
channel.messageComposer.updateConfig(patch) |
the channel's composer |
channel.messagePaginator.updateConfig(patch) |
the channel's message list |
channel.pinnedMessagesPaginator.updateConfig(patch) |
the channel's pinned-messages list |
channel.messageOperations.updateConfig(patch) |
how the channel sends messages |
Thread.updateConfig(patch) |
the thread |
thread.messageComposer.updateConfig(patch) |
the thread's composer |
thread.messagePaginator.updateConfig(patch) |
the thread's reply list |
thread.messageOperations.updateConfig(patch) |
how the thread sends replies |
channelPaginator.updateConfig(patch) |
one channel paginator your code created |
client.threads.paginator.updateConfig(patch) |
the thread list |
client.reminders.paginator.updateConfig(patch) |
the reminder list |
userGroupPaginator.updateConfig(patch) |
one user group list your code created |
client.threads.updateConfig(patch) |
the thread manager |
client.reminders.updateConfig(patch) |
the reminder manager |
client.notifications.updateConfig(patch) |
notifications |
client.messageDeliveryReporter.updateConfig(patch) |
delivery receipts |
client.connectionRecovery.updateConfig(patch) |
connection recovery |
client.wsConnection.updateConfig(patch) |
the WebSocket connection |
client.networkConnection.updateConfig(patch) |
the network status observer |
liveLocationManager.updateConfig(patch) |
the live location manager your code created |
searchController.updateConfig(patch) |
the search controller your code created |
A channel has no updateConfig(), so a channel's own settings, such as typing and read events, can be set only for every channel at once, in client.config.
Setup functions
Some customizations aren't values, such as adding composer middleware, or a setting that depends on the object. A setup function covers those. It's code that runs for every object of its kind, the existing ones and every one created later. It receives the object, and can change it, for example with MessageComposer.updateConfig():
client.config.setSetupFunction("messageComposer", ({ composer }) => {
// Composers in threads don't send typing events. Channel composers keep the default.
if (!composer.threadId) return;
composer.updateConfig({ text: { publishTypingEvents: false } });
});- There is one setup function per kind:
channel,thread,messageComposer,client,liveLocationManagerandsearchController. Setting another one replaces it, andnullremoves it. - It runs again whenever the settings of its kind change, so it can run more than once for the same object.
- An error it throws is logged and doesn't break the object.
- For a composer or a thread, it runs while the object's subscriptions are registered.
ChannelandThreadregister them for the composer they show, andChatregisters them for the threads the client keeps.
Teardown functions
A setup function can return a teardown function, which undoes what the setup function did. The teardown runs:
- before the setup function runs again for the same object;
- when you replace or remove the setup function, including with
client.config.reset(); - when the object is no longer used, for example a channel when it's disposed, a composer when the last
ChannelorThreadshowing it unmounts, and a thread when its subscriptions end.
Return one when running the setup function twice would do something twice. For example, a setup function that listens to an event adds a new listener on every run, so it returns the function that removes it:
client.config.setSetupFunction("channel", ({ channel }) => {
const { unsubscribe } = channel.on("message.new", (event) => {
console.log(`New message in ${channel.cid}`, event.message?.id);
});
return unsubscribe;
});Middleware inserted without unique: true is the same. Every run adds another copy, and the teardown removes it by its id:
import type { StreamChat } from "stream-chat";
import { voteActionsMiddleware } from "./voteActionsMiddleware";
export const setUpComposers = (client: StreamChat) =>
client.config.setSetupFunction("messageComposer", ({ composer }) => {
composer.compositionMiddlewareExecutor.insert({
middleware: [voteActionsMiddleware],
position: { after: "stream-io/message-composer-middleware/attachments" },
});
return () =>
composer.compositionMiddlewareExecutor.remove(voteActionsMiddleware.id);
});A setup function that gives the same result however often it runs doesn't need one. Calling updateConfig() is like that, and so is inserting middleware with unique: true, which replaces the copy an earlier run inserted:
import type { StreamChat } from "stream-chat";
import { voteActionsMiddleware } from "./voteActionsMiddleware";
export const setUpComposers = (client: StreamChat) =>
client.config.setSetupFunction("messageComposer", ({ composer }) => {
composer.compositionMiddlewareExecutor.insert({
middleware: [voteActionsMiddleware],
position: { after: "stream-io/message-composer-middleware/attachments" },
unique: true,
});
});Without a teardown, that middleware stays on the existing composers when you replace or remove the setup function, or call client.config.reset(). If it should go then, return a teardown that calls composer.compositionMiddlewareExecutor.remove(voteActionsMiddleware.id).
Composer middleware shows a setup function that adds middleware, with its teardown.
How the settings combine
Each object puts its settings together from everything above, in this order. A later step wins for the fields it sets:
- The SDK's defaults.
- Your settings in
client.config. - Settings passed to the constructor of an object your code creates.
- The setup function.
updateConfig()calls, including the ones a setup function makes.- The server's limits, last, so nothing above can go beyond them.
The object puts them together again whenever one of these changes. That happens when your code calls client.config.set(), client.config.setConfig(), client.config.reset() or client.config.setSetupFunction(), and, for channels and composers, when the channel's settings arrive from the server.
So a setting you change on one object with updateConfig() can be undone. Composers keep these changes, except after client.config.reset(). Other objects don't. A channel, for example, rebuilds the settings of its message list, its pinned-messages list and its message operations whenever the channel, messagePaginator or messageOperations key changes. For a change that must last, use a setup function, which runs again every time the settings are put together.
What the React Native SDK sets
The SDK's components register a few settings of their own:
- The network status reporter.
Chatsetsclient.networkConnection.statusReportertonetInfoStatusReporterwhen it mounts, unless a reporter is registered there already. It reports the device's network through@react-native-community/netinfo.useCreateChatClientalso passes it to the constructor, so the client has it from the start. A client you create yourself gets it only whenChatmounts, unless you passnetInfoStatusReporterfromstream-chat-react-nativein the tree you give the constructor. See Connection. client.persistUserOnConnectionFailure.Chatsets it to the value of itsenableOfflineSupportprop,falseby default, when it mounts and whenever the prop changes. It overwrites a value you set before. See Offline support.messageComposer.attachments.pendingUploadsEnabled. WithenableOfflineSupport,Chatsets it totrue, unless you set it yourself. It never sets it back. See Sending with pending uploads.- The message list's page size.
Channelsets the page size of its channel's message list to 25 when it mounts, unless apageSizeis set undermessagePaginatororchannel.messagePaginatorinclient.config. It sets it on the list itself, so the channel drops it when the list's settings are rebuilt, as described above. The list then loads 100 messages a page until the channel opens again. Set the page size inclient.configto keep it.
Settings read once
A few settings are read only when an object is created. Setting them later doesn't change the objects that exist already, and the client logs a warning:
axiosRequestConfig,baseURLandnotificationManagerunder theclientkey. Pass them in the tree you create the client with.initialCursor,initialOffsetandunreadReferencePolicyof a message list, andinitialCursorandinitialOffsetof the pinned-messages list. Set them before the channels and threads are created, in the tree you create the client with.
Two composer settings are also read only when a composer is built, but without a warning: linkPreviews.debounceURLEnrichmentMs and location.getDeviceId. Set them before the composers are created.
Server settings
These live on the server. The client receives them with the channel or with the user.
- Channel type settings, set in the Stream Dashboard or through the server API:
typing_events,read_events,replies,user_message_reminders,delivery_events,uploads,url_enrichment,shared_locations,polls,max_message_lengthandcommands. A channel's ownconfig_overridescan narrow them for that channel alone. They arrive with the channel and are inchannel.serverConfig, which isundefineduntil the channel has been queried or watched. - Capabilities say what the current user may do in each channel, from their roles and the channel type's permissions. They're in
channel.stateasownCapabilities, a list such as"send-message","upload-file"and"send-reaction". When they change,channel.stateupdates. After a channel query orChannel.updatePartial()returns different capabilities, the client also raisescapabilities.changed. The server doesn't send it. - Per-channel settings are stored with the channel. Slow mode is one:
channel.data.cooldownholds the seconds between two messages. A user with the"skip-slow-mode"capability isn't held back. - The user's privacy settings for typing indicators and delivery receipts are in
client.user.privacy_settings. When the user turns one off, the client doesn't send those events for them.
Who decides what
Some settings belong to the client alone, some to the server alone, and some need both sides to allow them.
Only the client decides:
- the page size, update throttling, memory cap and unread policy of message lists (
messagePaginator); - how messages are sent, through the request handlers (
channel.requestHandlersandthread.requestHandlers), and how many failed sends are kept for retry, and for how long (messageOperations); - the composer's features, which are drafts, sending before uploads finish, a custom CDN or upload function, the accepted files and their number, how links are found and how long to wait before fetching a preview, the location device id and the shortest sharing time, command validation, the default text, and
retainCompositionOnSubmit; - timing for the connection, recovery, notifications and reminders;
- search.
Only the server decides capabilities, slow mode, the user's privacy settings, and the list of commands a channel offers, which is in channel.config.availableCommands.
Both sides must allow it. The server can only narrow what the client turns on, so a feature is on when both say yes:
| Client setting | Server setting |
|---|---|
channel.typingEvents.enabled |
typing_events |
channel.readEvents.enabled |
read_events |
channel.replies.enabled |
replies |
channel.userMessageReminders.enabled |
user_message_reminders |
channel.deliveryEvents.enabled |
delivery_events |
messageComposer.attachments.enabled |
uploads, unless attachments.customCdn is true |
messageComposer.linkPreviews.enabled |
url_enrichment |
messageComposer.location.enabled |
shared_locations |
messageComposer.polls.enabled |
polls |
messageComposer.text.maxLengthOnSend and maxLengthOnEdit |
max_message_length, which caps both |
The value in effect is on each object. See Checking whether a feature is on.
Checking whether a feature is on
Every configurable object, the channel, the thread, the composer, the paginators and the client's managers, has the same members for its settings:
configholds the settings in effect, the server's limits included. Read it.configStateis the store that holds them. Subscribe to it to follow changes, for example when a channel's server settings arrive with its first query.updateConfig(patch)changes this object only, see Settings for one object. A channel doesn't have it.
The managers inside a composer, such as attachmentManager and linkPreviewsManager, read their part of the composer's settings, so follow them through the composer's configState.
Don't check client.config or channel.serverConfig for this. client.config holds only what your app set, and serverConfig only what the server allows. Only the object's config combines both.
For example, to know whether uploads are allowed now and whenever that changes:
const composer = channel.messageComposer;
// Now
const canAttach = composer.config.attachments.enabled;
// From now on, whenever it changes
const unsubscribe = composer.configState.subscribeWithSelector(
({ attachments }) => ({ enabled: attachments.enabled }),
({ enabled }) => {
console.log(enabled ? "Uploads allowed" : "Uploads not allowed");
},
);A component that shows or hides an attach button reads the same value with useStateStore(composer.configState, selector). See Reading state in components.
Some features also need a capability. To decide whether to offer polls, check the composer's setting and the user's capability:
const canCreatePolls =
channel.messageComposer.config.polls.enabled &&
channel.state.getLatestValue().ownCapabilities.includes("send-poll");