Use Ably in your React application using idiomatic, easy to use, React Hooks!
Using this module you can:
- Interact with Ably channels using a React Hook.
- Publish messages via Ably using the publish function the hooks provide
- Get notifications of user presence on channels
- Enter presence and send presence updates
The hooks provide a simplified syntax for interacting with Ably, and manage the lifecycle of the Ably SDK instances for you taking care to subscribe and unsubscribe to channels and events when your react components re-render.
Note
For more information what Ably is and concepts you need, please see the official Ably documentation. The official docs also include a more complete React Hooks quickstart guide.
The hooks are compatible with all versions of React above 16.8.0
Start by connecting your app to Ably using the AblyProvider
component. See the ClientOptions
documentation for information about what options are available when creating an Ably client. If you want to use the usePresence
or usePresenceListener
hooks, you'll need to explicitly provide a clientId
.
The AblyProvider
should be high in your component tree, wrapping every component which needs to access Ably.
Also, ensure that the Ably.Realtime
instance is created outside of components to prevent it from being recreated on component re-renders. This will help avoid opening extra unnecessary connections to the Ably servers and potentially reaching the maximum connections limit on your account.
import { AblyProvider } from 'ably/react';
import * as Ably from 'ably';
const client = new Ably.Realtime({ key: 'your-ably-api-key', clientId: 'me' });
root.render(
<AblyProvider client={client}>
<App />
</AblyProvider>,
);
Once you've set up AblyProvider
, define Ably channels you want to use by utilizing the ChannelProvider
component:
<ChannelProvider channelName="your-channel-name">
<Component />
</ChannelProvider>
After setting up ChannelProvider
, you can employ the provided hooks
in your code. Here's a basic example:
const { channel } = useChannel('your-channel-name', (message) => {
console.log(message);
});
Every time a message is sent to your-channel-name
it'll be logged to the console. You can do whatever you need to with those messages.
Note
Our react hooks are designed to run on the client-side, so if you are using server-side rendering, make sure that your components which use Ably react hooks are only rendered on the client side.
We support providing ChannelOptions for the ChannelProvider
component:
This means you can use features like rewind
:
<ChannelProvider channelName="your-channel-name" options={{ params: { rewind: '1' } }}>
<Component />
</ChannelProvider>
Subscription filters are also supported:
const deriveOptions = { filter: 'headers.email == `"[email protected]"` || headers.company == `"domain"`' }
return (
<ChannelProvider channelName="your-derived-channel-name" options={{ ... }} deriveOptions={deriveOptions}>
<Component />
</ChannelProvider>
)
Note
Please note that attempts to publish to a derived channel (the one created or retrieved with a filter expression)
using channel instance will fail, since derived channels support only subscribe
capability.
Use publish
function returned by useChannel
hook instead.
The useChannel hook lets you subscribe to an Ably Channel and receive messages from it.
const { channel, ably } = useChannel('your-channel-name', (message) => {
console.log(message);
});
Both the channel instance, and the Ably JavaScript SDK instance are returned from the useChannel call.
useChannel
really shines when combined with a regular react useState
hook - for example, you could keep a list of messages in your app state, and use the useChannel
hook to subscribe to a channel, and update the state when new messages arrive.
const [messages, updateMessages] = useState([]);
const { channel } = useChannel('your-channel-name', (message) => {
updateMessages((prev) => [...prev, message]);
});
// Convert the messages to list items to render in a react component
const messagePreviews = messages.map((msg, index) => <li key={index}>{msg.data.someProperty}</li>);
useChannel
supports all of the parameter combinations of a regular call to channel.subscribe
, so you can filter the messages you subscribe to by providing a message type
to the useChannel
function:
useChannel('your-channel-name', 'test-message', (message) => {
console.log(message); // Only logs messages sent using the `test-message` message type
});
The publish
function returned by useChannel
can be used to send messages to the channel.
const { publish } = useChannel('your-channel-name');
publish('test-message', { text: 'message text' });
The useChannel
hook returns an instance of the channel, which is part of the Ably JavaScript SDK. This allows you to access the standard Ably JavaScript SDK functionalities associated with channels.
By providing both the channel instance and the Ably SDK instance through our useChannel hook, you gain the flexibility to execute various operations on the channel.
For instance, you can easily fetch the history of the channel using the following method:
const { channel } = useChannel('your-channel-name', (message) => {
console.log(message);
});
const history = channel.history((err, result) => {
var lastMessage = resultPage.items[0];
console.log('Last message: ' + lastMessage.id + ' - ' + lastMessage.data);
});
The usePresence hook enters the presence set on a channel and enables you to send presence updates for current client. To find out more about Presence, see the Presence documentation.
const { updateStatus } = usePresence('your-channel-name');
// The `updateStatus` function can be used to update the presence data for the current client
updateStatus('status');
You can optionally provide a second parameter when you usePresence
to set an initial presence data
.
const { updateStatus } = usePresence('your-channel-name', 'initial state');
// The `updateStatus` function can be used to update the presence data for the current client
updateStatus('new status');
The new state will be sent to the channel, and all clients subscribed to the channel (including current, if you've subscribed to updates using usePresenceListener
hook) will be notified of the change immediately.
usePresence
supports objects and numbers, as well as strings:
usePresence('your-channel-name', { foo: 'bar' });
usePresence('another-channel-name', 123);
usePresence('third-channel-name', 'initial status');
If you're using TypeScript
there are type hints to make sure that value passed to updateStatus
is of the same type
as your initial constraint, or a provided generic type parameter:
const TypedUsePresenceComponent = () => {
// In this example MyPresenceType will be used for type checking updateStatus function.
// If omitted, the shape of the initial value will be used, and if that's omitted, `any` will be the default.
const { updateStatus } = usePresence<MyPresenceType>('testChannelName', { foo: 'bar' });
return (
<button
onClick={() => {
{
/* you will have Intellisense for updateStatus function here */
}
updateStatus({ foo: 'baz' });
}}
>
Update presence data
</button>
);
};
interface MyPresenceType {
foo: string;
}
The usePresenceListener hook subscribes you to presence 'enter', 'update' and 'leave' events on a channel - this will allow you to get notified when a user joins or leaves the channel, or updates its presence data. To find out more about Presence, see the Presence documentation.
Please note that fetching present members is executed as an effect, so it'll load in after your component renders for the first time.
const { presenceData } = usePresenceListener('your-channel-name');
// Convert presence data to the list of items to render
const membersData = presenceData.map((msg, index) => (
<li key={index}>
{msg.clientId}: {msg.data}
</li>
));
The usePresenceListener
hook returns an array of presence messages - each message is a regular Ably JavaScript SDK PresenceMessage
instance.
If you don't want to use the presenceData
returned from usePresenceListener
, you can configure a callback:
usePresenceListener('your-channel-name', (presenceUpdate) => {
console.log(presenceUpdate);
});
If you're using TypeScript
there are type hints to make sure that presence data received are of the same type
as a provided generic type parameter:
const TypedUsePresenceListenerComponent = () => {
// In this example MyPresenceType will be used for presenceData type hints.
// If that's omitted, `any` will be the default.
const { presenceData } = usePresenceListener<MyPresenceType>('testChannelName');
const membersData = presenceData.map((presenceMsg, index) => {
return (
<li key={index}>
{/* you will have Intellisense for presenceMsg.data of type MyPresenceType here */}
{presenceMsg.clientId} - {presenceMsg.data.foo}
</li>
);
});
return <ul>{membersData}</ul>;
};
interface MyPresenceType {
foo: string;
}
presenceData
is a good way to store synchronised, per-client metadata, so types here are especially valuable.
The useConnectionStateListener
hook lets you attach a listener to be notified of connection state changes. This can be useful for detecting when the client has lost connection.
useConnectionStateListener((stateChange) => {
console.log(stateChange.current); // the new connection state
console.log(stateChange.previous); // the previous connection state
console.log(stateChange.reason); // if applicable, an error indicating the reason for the connection state change
});
You can also pass in a filter to only listen to a set of connection states:
useConnectionStateListener('failed', listener); // the listener only gets called when the connection state becomes failed
useConnectionStateListener(['failed', 'suspended'], listener); // the listener only gets called when the connection state becomes failed or suspended
The useChannelStateListener
hook lets you attach a listener to be notified of channel state changes. This can be useful for detecting when a channel error has occured.
useChannelStateListener((stateChange) => {
console.log(stateChange.current); // the new channel state
console.log(stateChange.previous); // the previous channel state
console.log(stateChange.reason); // if applicable, an error indicating the reason for the channel state change
});
You can also pass in a filter to only listen to a set of channel states:
useChannelStateListener('failed', listener); // the listener only gets called when the channel state becomes failed
useChannelStateListener(['failed', 'suspended'], listener); // the listener only gets called when the channel state becomes failed or suspended
The useAbly
hook lets you access the Ably client used by the AblyProvider
context. This can be useful if you need to access ably-js APIs which aren't available through our react-hooks library.
const client = useAbly();
client.authorize();
When using the Ably react hooks, your Ably client may encounter a variety of errors, for example if it doesn't have permissions to attach to a channel it may encounter a channel error, or if it loses connection from the Ably network it may encounter a connection error.
To allow you to handle these errors, the useChannel
, usePresence
and usePresenceListener
hooks return connection and channel errors so that you can react to them in your components:
const { connectionError, channelError } = useChannel('my_channel', messageHandler);
if (connectionError) {
// TODO: handle connection errors
} else if (channelError) {
// TODO: handle channel errors
} else {
return <AblyChannelComponent />;
}
Alternatively, you can also pass callbacks to the hooks to be called when the client encounters an error:
useChannel(
{
channelName: 'my_channel',
onConnectionError: (err) => {
/* handle connection error */
},
onChannelError: (err) => {
/* handle channel error */
},
},
messageHandler,
);
If you need to use multiple Ably clients on the same page, the easiest way to do so is to keep your clients in separate AblyProvider
components. However, if you need to nest AblyProvider
s, you can pass a string ablyId
for each client as a prop to the provider.
root.render(
<AblyProvider client={client1} ablyId={'providerOne'}>
<AblyProvider client={client2} ablyId={'providerTwo'}>
<App />
</AblyProvider>
</AblyProvider>,
);
This ablyId
can then be passed in to each hook to specify which client to use.
const ablyId = 'providerOne';
const client = useAbly(ablyId);
useChannel({ channelName: 'your-channel-name', ablyId }, (message) => {
console.log(message);
});
usePresence({ channelName: 'your-channel-name', ablyId }, 'initial state');
usePresenceListener({ channelName: 'your-channel-name', ablyId }, (presenceUpdate) => {
// ...
});
By default, usePresenceEnter
and usePresenceListener
automatically attach to a channel upon component mount, and useChannel
does so if a callback for receiving messages is provided. This means that these hooks will attempt to establish a connection to the Ably server using the credentials currently set in the corresponding Ably.RealtimeClient
from AblyProvider
. However, there may be scenarios where your user authentication is asynchronous or when certain parts of your application are conditionally accessible (e.g., premium features). In these instances, you might not have a valid auth token yet, and an attempt to attach to a channel would result in an error.
To address this, the skip
parameter allows you to control the mounting behavior of the useChannel
, usePresenceEnter
, and usePresenceListener
hooks, specifically determining whether they should attach to a channel upon the component's mount.
const [skip, setSkip] = useState(true);
const { channel, publish } = useChannel({ channelName: 'your-channel-name', skip }, (message) => {
updateMessages((prev) => [...prev, message]);
});
By setting the skip
parameter, you can prevent the hooks from attempting to attach to a channel, thereby avoiding errors and avoiding unnecessary messages being send (which reduces your messages consumption in your Ably app).
The skip
parameter accepts a boolean value. When set to true
, it instructs the hooks not to attach to the channel upon the component's mount. This behavior is dynamically responsive; meaning that, once the conditions change (e.g., the user gets authenticated), updating the skip
parameter to false
will trigger the hooks to attach to the channel.
This parameter is useful in next situations:
Asynchronous Authentication: Users are not immediately authorized upon loading the component, and acquiring a valid auth token is asynchronous.
Consider a scenario where a component uses the useChannel
hook, but the user's authentication status is determined asynchronously:
import React, { useEffect, useState } from 'react';
import { useChannel } from 'ably/react';
import * as Ably from 'ably';
const ChatComponent = () => {
const [isUserAuthenticated, setIsUserAuthenticated] = useState(false);
const [messages, updateMessages] = useState<Ably.Message[]>([]);
// Simulate asynchronous authentication
useEffect(() => {
async function authenticate() {
const isAuthenticated = await someAuthenticationFunction();
setIsUserAuthenticated(isAuthenticated);
}
authenticate();
}, []);
// useChannel with skip parameter
useChannel({ channelName: 'your-channel-name', skip: !isUserAuthenticated }, (message) => {
updateMessages((prev) => [...prev, message]);
});
if (!isUserAuthenticated) {
return <p>Please log in to join the chat.</p>;
}
return (
<div>
{messages.map((message, index) => (
<p key={index}>{message.data.text}</p>
))}
</div>
);
};
Conditional Feature Access: Certain features or parts of your application are behind a paywall or require specific user privileges that not all users possess.
In an application with both free and premium features, you might want to conditionally use channels based on the user's subscription status:
import React from 'react';
import { useChannel } from 'ably/react';
import * as Ably from 'ably';
interface PremiumFeatureComponentProps {
isPremiumUser: boolean;
}
const PremiumFeatureComponent = ({ isPremiumUser }: PremiumFeatureComponentProps) => {
const [messages, updateMessages] = useState<Ably.Message[]>([]);
// Skip attaching to the channel if the user is not a premium subscriber
useChannel({ channelName: 'premium-feature-channel', skip: !isPremiumUser }, (message) => {
updateMessages((prev) => [...prev, message]);
});
if (!isPremiumUser) {
return <p>This feature is available for premium users only.</p>;
}
return <div>{/* Render premium feature based on messages */}</div>;
};
Currently, when using our react library with NextJS you may encounter some warnings which arise due to some static checks against subdependencies of the library. While these warnings won't affect the performance of the library and are safe to ignore, we understand that they are an annoyance and offer the following advice to prevent them from displaying:
This warning comes from keyv which is a subdependency of our NodeJS http client. You can read more about the reason this warning is displayed at jaredwray/keyv#45.
You can avoid this warning by overriding the version of keyv used by adding the following to your package.json:
"overrides": {
"cacheable-request": {
"keyv": "npm:@keyvhq/core@~1.6.6"
}
}
These warnings come from devDependencies which are conditionally loaded in the ws module (our NodeJS websocket client). They aren't required for the websocket client to work, however NextJS will statically analyse imports and incorrectly assume that these are needed.
You can avoid this warning by adding the following to your next.config.js:
module.exports = {
webpack: (config) => {
config.externals.push({
'utf-8-validate': 'commonjs utf-8-validate',
bufferutil: 'commonjs bufferutil',
});
return config;
},
};