Using useSharedState
A hook that syncs the state across all clients. All clients see and (if permitted) modify the same "shared" state.
Accessing & Updating State
It works exactly the same as React's useState
const [state, setState] = useSharedState<boolean>(false);
// the first element has the state
state === false;
// this one sets the state
setState(true);
By default, the unique identifier for this piece of state (known as a channel
) is set to be the
pathname
derived from window.location.href
An important limitation of this is that you can only have at most one SharedState without
an explicitly defined channel
on a page.
Functional Updates
When you have more complex state, it is more recommended to do functional updates instead because it is better suited to handle race conditions while syncing.
const [details, setDetails] = useSharedState({
name: 'Mark S.',
address: {
city: 'Denver',
state: 'CO'
}
});
setDetails(details => {
return {
...details,
address: {
...details.address,
city: 'Boulder'
}
};
});
Setting Initial State
Much like React's useState
you can pass in both a raw state or a function that
returns derived state.
const [state, setState] = useSharedState('Hello');
state === 'Hello';
const [state, setState] = useSharedState(() => 'Hello'.toUpperCase());
state === 'HELLO';
Explicitly Defining Channel
We allow you to not define a channel
explicitly (by inferring it from pathname
) to make your
first experience with SharedState magical ✨, but it's highly recommended to always
define a channel explicitly.
export function App() {
const [message, setMessage] = useSharedState('Hello', {
channel: 'UNIQUE IDENTIFIER FOR THIS PIECE OF STATE'
});
return <></>;
}
As mentioned above, channel
is by default set to the current URL's pathname. You can only use
one SharedState instance without the channel specified explicitly.
Validating State
As state can be set by the client side, it's possible to break your code accidentally by writing the wrong shape of state from two different components.
In the below example we're using zod, but you can welcome to use any schema validation library to validate the state.
import z from 'zod';
export function App() {
const [message, setMessage, { error }] = useSharedState<string>('Hello', {
channel: 'CHANNEL ID',
validate: (rawState: any): string => {
return z.string().parse(rawState);
}
});
return (
<pre>
{error.message ?? message}
</pre>
);
}
- The generic argument for useSharedState and the return type of
validate
must match. - If you skip the generic argument, the return type of
validate
will be inferred to be the type of state.
Handling Errors
If there ever happens to be an error either in validation or with the underlying
mechanisms, error
would be not null nor undefined, and the state
will either
be the last known "good" state or initial (computed) state.
const [state, setState, { error }] = useSharedState<string>('Hello', {
validate: (rawState) => {
if (typeof rawState !== 'string') {
throw new Error('state must be a string');
}
return rawState;
}
});
console.assert(state === 'Hello');
if (error) {
// do something with `error`
}
The error
property above will only contain the last error, and will be cleared as soon
as another "good" state is received. If you want to persist errors, you are welcome to
use the onError
option to DIY.
const [storedErrors, setStoredErrors] = useState<any[]>([]);
const [state, setState] = useSharedState<string>('Hello', {
onError(error) {
setStoredErrors(errors => [...errors, error]);
}
});
Underlying Connection Info
Sometimes it is necessary to be able to retrieve and act on the state of the underlying mechanisms involved.
export function App() {
const [message, setMessage, { connected, started, synced, error }] = useSharedState<string>('Hello', {
channel: 'CHANNEL ID',
});
}
Key | Type | Description |
---|---|---|
connected | boolean | true if the underlying client connection is open. |
started | boolean | true if the underlying subscription on the server for channel is active. |
synced | boolean | true if the first sync of the underlying y.Doc has happened. |
error | any | The most recent error in the underlying connection or subscription or validation |
It is recommended to start setting state after the first sync has occurred,
i.e. when synced
is true
Deferring Sync Start
Sometimes you might want to enable syncing after a delay maybe because you want to derive the
channel
via an asynchronous process. For such instances you may use the enabled
option.
const [channel, setChannel] = useState<null | string>(null);
useEffect(() => {
// for illustrative purposes of course.
setTimeout(() => {
setChannel('Realtime Hello');
}, 1000);
}, []);
const [message, setMessage] = useSharedState('Hello', {
channel: channel!,
enabled: channel !== null
});
channel
is only read once when the subscription is being started, hence
the channel
is read once when you set enabled
to true, but changing channel
after the fact will not change the channel being subscribed to.
Tokens & Security
By default, AirState servers allow all clients to read and write to any channel, but that can be locked down via config. If the default permissions don't permit reads and writes, your backend will need to provide a signed token.
You can pass this signed token from your backend to the useSharedState
hook via the
token
option.
Literal String
// you can pass the token directly if
// you already have it
const [message, setMessage] = useSharedState('Hello', {
token: 'TOKEN STRING'
});
Function
// you can pass a function that returns
// the (latest) token
const [message, setMessage] = useSharedState('Hello', {
token: () => {
return 'TOKEN STRING';
}
});
Async Function
// that function can be async
// if you want
const [message, setMessage] = useSharedState('Hello', {
token: async () => {
const req = await fetch('/token/route');
const resBody = await req.json();
return resBody.token;
}
});
Custom Client
By default, AirState's hooks will use the "default client" that is created the first time you invoke a subscription. This client is global and not easy to re-configure once already configured.
Having said that, you can still pass a different client created with createClient
via
the client
option.
import {createClient} from "@airstate/client";
const differentClient = createClient({
// client options
});
export function App() {
const [state, setState] = useSharedState('', {
client: differentClient
});
return <></>;
}