import { StreamChat } from "stream-chat";
const client = StreamChat.getInstance("api_key");
State Overview
Stream Chat for React Native uses the Stream Chat client to connect to and communicate with the Stream API.
The full JavaScript client docs should be referenced for detailed information on directly using the client.
Setup
To interact with the Stream Chat API you must create a client instance and connect to the API, usually as an authenticated user.
Instantiation
The Stream Chat client, StreamChat
is a dependency of stream-chat-react-native
and can be imported from stream-chat
.
To create a client instance you can call getInstance
on the client and provide and API key.
Usage of StreamChat.getInstance()
available since stream-chat@2.12.0.
This new Singleton pattern allows you to instantiate a unique StreamChat client, i.e create a StreamChat instance and retrieve it wherever you need it on your app to perform API calls. After calling it once, any following
getInstance
call will return the initial StreamChat instance. This will prevent you from accidentally creating multiple
StreamChat instances, opening multiple WebSockets, and driving up your concurrent connections unnecessarily.
Stream Chat is backward compatible. Users can continue using new StreamChat()
if desired.
const client = new StreamChat("api_key");
Calling new StreamChat()
repeatedly will create new copies of the client and in turn new WebSocket connections when connectUser
is called.
If you are using new StreamChat()
you need to be vigilant about your use to ensure you are not creating multiple WebSocket connections unnecessarily.
Connecting a User
To connect a user a the connectUser
function should be called and provided with the user object and user_token
.
The user_token
is typically sent from your back end when a user is authenticated.
await client.connectUser(
{
id: "testUser",
name: "Test User",
},
"user_token",
);
It is recommended to not repeatedly call connectUser
unnecessarily, multiple calls to connectUser
will result in warnings, and attempting to call connectUser
before disconnecting a current user will throw an Error.
UI Components
The Stream Chat for React Native UI components handle most interactions with the client for you after connecting with an authenticated user.
Providing the Client to the UI
To provide the client to the UI components you simply provide the client instance as the prop client
to the Chat
component.
import { StreamChat } from "stream-chat";
import { Chat } from "stream-chat-react-native";
const client = StreamChat.getInstance("api_key");
export const Screen = () => (
<Chat client={client}>{/** App components */}</Chat>
);
The Stream Chat for React Native UI components then handle interacting with the client internally by accessing the client via context
.
If you are customizing certain components or functionality you may have to interact with the client as well.
You can access the client provided to the Chat
component internally via the useChatContext
.
import { useChatContext } from "stream-chat-react-native";
const { client } = useChatContext();
Using UI Functions
The UI provides a number of functions that interact with the client while keeping the UI state in sync using context
.
The contexts section details what functions are accessible.
When customizing your application you should ensure you are utilizing the correct functions to keep the UI state up to date.
The sendMessage
function for instance, provided by useMessageInputContext
, is not the same as the sendMessage
function found directly on a channel
in the client.
Therefore calling channel.sendMessage(messageData)
will not result in a message optimistically showing in the UI, or a failed send state, instead the message will not show until it is returned by the server.
You should not assume any function directly called on the client will result in a UI update.
The UI state is managed internally by the components and context
, most client interactions require an event returned by the server to update the UI.
Accessing the Client Instance
There are multiple ways to access the client instance throughout your application.
As mentioned you can access the client
via useChatContext
when inside the Chat
component.
This works well if you can wrap your entire application in a single Chat
component and have the StreamChat
instance provided throughout your app via the internal context
.
But if you have multiple screens that contain Chat
components where a client instance is necessary you will need to access the shared instance in other ways.
You can store the client in a context
you create yourself.
Create your own custom class that provides it.
Or using the Singleton structure you can call getInstance
when required to always be returned the current instance if one exists, or create a new one otherwise.
Do not create and connect multiple instances using new StreamChat()
, this will result in multiple StreamChat
instances and opening multiple WebSocket connections.
Direct Interaction
There may be some direct interaction with the client that is required for your application. Referring to the full documentation is suggested for detailed information on client functionality. Common interactions with the client used in conjunction with the Stream Chat for React Native components have been included for convenience.
Creating a Channel
A channel must be initialized with either an id
or a list of members for the channel.
If you provide a list of members an id
will be auto-generated on backend for the channel.
You can’t add or remove members from channel created using a list of members.
/**
* Channel created using a channel id
*/
const channel = client.channel(channel_type, "channel_id", {
name: "My New Channel",
});
/**
* Channel created using a members list
*/
const channel = client.channel(channel_type, {
members: ['userOne', 'userTwo']
name: 'My New Channel',
});
To create a channel on the server you must call create
or watch
on a new channel instance.
create
will create the channel while watch
will both create the channel and subscribe the client to updates on the channel.
await channel.watch();
await channel.create();
Querying ChannelList
To show a list of channels for the user you can use the following guide.
Internally, a client has queryChannels
API that can be used to get the list of channels.
const filter = { type: "messaging", members: { $in: ["thierry"] } };
const sort = [{ last_message_at: -1 }];
const channels = await chatClient.queryChannels(filter, sort, {
watch: true, // this is the default
state: true,
});
Sending messages
To send a message in the channel, you can refer to the following guide.
Internally, a channel instance has sendMessage
API that has to be called to send a message, as below:
const message = await channel.sendMessage({
text: "Hey there.",
});
Disconnecting a User
To disconnect a user you can call disconnect
on the client.
await client.disconnectUser();
POJO
With a few of our new features, we’ve decided to refresh our state management and moved to a subscribable POJO with selector based system to make developer experience better when it came to rendering information provided by our StreamChat
client.
This change is currently only available within StreamChat.threads
, StreamChat.poll
and StreamChat.polls
but will be reused across the whole SDK later on.
Why POJO (State Object)
Our SDK holds and provides A LOT of information to our integrators and each of those integrators sometimes require different data or forms of data to display to their users. All of this important data now lives within something we call state object and through custom-tailored selectors our integrators can access the combination of data they require without any extra overhead and performance to match.
What are Selectors
Selectors are functions provided by integrators that run whenever state object changes. These selectors should return piece of that state that is important for the appropriate component that renders that piece of information. Selectors itself should not do any heavy data computations that could resolve in generating new data each time the selector runs (arrays and objects), use pre-built hooks with computed state values or build your own if your codebase requires it.
Rules of Selectors
- Selectors should return a named object.
const selector = (nextValue: ThreadManagerState) => ({
unreadThreadsCount: nextValue.unreadThreadsCount,
active: nextValue.active,
lastConnectionDownAt: nextvalue.lastConnectionDownAt,
});
- Selectors should live outside components scope or should be memoized if it requires “outside” information (
userId
forread
object for example). Not memoizing selectors (or not stabilizing them) will lead to bad performance as each time your component re-renders, the selector function is created anew anduseStateStore
goes through unsubscribe and resubscribe process unnecessarily.
// ❌ not okay
const Component1 = () => {
const { latestReply } = useStateStore(
thread.state,
(nextValue: ThreadState) => ({
latestReply: nextValue.latestReplies.at(-1),
}),
);
return <Text>{latestReply.text}</Text>;
};
// ✅ okay
const selector = (nextValue: ThreadState) => ({
latestReply: nextValue.latestReplies.at(-1),
});
const Component2 = () => {
const { latestReply } = useStateStore(thread.state, selector);
return <Text>{latestReply.text}</Text>;
};
// ✅ also okay
const Component3 = ({ userId }: { userId: string }) => {
const selector = useCallback(
(nextValue: ThreadState) => ({
unreadMessagesCount: nextValue.read[userId].unread_messages,
}),
[userId],
);
const { unreadMessagesCount } = useStateStore(thread.state, selector);
return <Text>{unreadMessagesCount}</Text>;
};
- Break your components down to the smallest reasonable parts that each take care of the appropriate piece of state if it makes sense to do so.
Accessing Reactive State
Our SDK currently allows to access two of these state structures - in Thread and ThreadManager instances under state
property.
Vanilla
import { StreamChat } from "stream-chat";
const client = new StreamChat(/*...*/);
// calls console.log with the whole state object whenever it changes
client.threads.state.subscribe(console.log);
let latestThreads;
client.threads.state.subscribeWithSelector(
// called each time theres a change in the state object
(nextValue) => ({ threads: nextValue.threads }),
// called only when threads change (selected value)
({ threads }) => {
latestThreads = threads;
},
);
// returns lastest state object
const state = client.threads.state.getLatestValue();
const [thread] = latestThreads;
// thread instances come with the same functionality
thread?.state.subscribe(/*...*/);
thread?.state.subscribeWithSelector(/*...*/);
thread?.state.getLatestValue(/*...*/);
useStateStore Hook
For the ease of use - the React Native SDK comes with the appropriate state access hook which wraps StateStore.subscribeWithSelector
API for the React-based applications.
import { useStateStore } from "stream-chat-react-native";
import type { ThreadManagerState } from "stream-chat";
const selector = (nextValue: ThreadManagerState) =>
({ threads: nextValue.threads }) as const;
const CustomThreadList = () => {
const { client } = useChatContext();
const { threads } = useStateStore(client.threads.state, selector);
return (
<View>
{threads.map((thread) => (
<Text key={thread.id}>{thread.id}</Text>
))}
</View>
);
};
Thread and ThreadManager
One of the feature that follows the new POJO style of state management is the threads feature.
It provides a reactive state for both a single Thread
(through a reactive threadInstance
) and a list of threads (through StreamChat.threads
).
Both states can be accessed through selector
s as outlined in the examples above.
Poll and PollManager
Our new polls feature also follows the new POJO style of state management. A poll
in itself is something that needs to be linked to a message
in order for it to work. When a poll is created, the only way to make it visible to the users is to send it as a message. This message
needs to have a poll_id
attached to it and preferably no text.
You can access each poll
’s reactive state by getting it by ID using StreamChat.polls.fromState(<poll-id>)
.
Please keep in mind that message.poll
is not going to be reactive, but will rather contain the raw poll
data as returned by our backend.
Utility hooks
The React Native SDK provides 2 utility hooks to help with consuming the poll
state. They can be found listed below:
Similarly to the threads
feature, one can also directly use useStateStore
and access StreamChat.polls.fromState(<poll-id>).state
through custom selector
s.
Both usePollStateStore
and usePollState
can only be used in children of a PollContext
. This impediment does not exist however on useStateStore
.
Due to this, all poll
related components within the SDK are self-wrapped within a PollContext
and require message
and poll
as mandatory props.
Example
import { usePollState } from "stream-chat-react-native";
const CustomPollComponent = () => {
const { name, options } = usePollState();
return (
<View>
<Text>{name}</Text>
{options.map((option) => (
<Text key={option.id}>{option.text}</Text>
))}
</View>
);
};
const PollMessage = ({ message }) => {
const { client } = useChatContext();
const pollInstance = client.polls.fromState(message?.poll_id);
return (
<PollContextProvider value={{ message, poll: pollInstance }}>
<CustomPollComponent />
</PollContextProvider>
);
};