restate / v1.0.0
State owned by a component
Give each document, dialog or request its own stores that are created with the component and released after it unmounts.
On this page
useRestateScopeRules of the factoryInitial values from propsServer renderingWait for a document’s saved dataTry itOwners outside ReactuseRestateScope
The factory declares stores and returns them. restate keeps the result across renders and destroys the scope after the component unmounts. There is no close function or cleanup effect to write.
Two Document elements have independent state. To share a document with its children, pass the returned hooks through props or your own context.
import { restate, useRestateScope } from '@kkwaenggwari/restate';
function Document() {
const { useDraft, useSelection } = useRestateScope((scope) => ({
useDraft: restate('', { scope }),
useSelection: restate(0, { scope }),
}));
const text = useDraft();
const selection = useSelection();
return (
<section>
<input value={text} onChange={(event) => useDraft.set(event.target.value)} />
<button onClick={() => useSelection.set((n) => n + 1)}>Selection: {selection}</button>
</section>
);
}Rules of the factory
React may call the factory more than once and discard results, so keep it synchronous and pure.
- Declare stores in the factory; call the returned hooks at the top level of the component.
- Initial props are captured once, like a useState initializer.
- Give the component a key when a different document needs fresh state.
- Without a factory, useRestateScope() returns a stable scope for stores you create later in effects or handlers.
Initial values from props
Use props as initial values and a React key to start over for another document.
function Editor({ document }) {
// The initial value is captured once, like a useState initializer.
const useTitle = useRestateScope((scope) => restate(document.title, { scope }));
const title = useTitle();
return <input value={title} onChange={(event) => useTitle.set(event.target.value)} />;
}
// A different document gets fresh state.
<Editor key={document.id} document={document} />;Server rendering
Load the data in your server route and pass it as a prop. The initial value also supplies the server snapshot. In the Next.js App Router, this hook belongs in a Client Component, which is still server rendered.
'use client';
import { restate, useRestateScope } from '@kkwaenggwari/restate';
export default function Profile({ user }) {
const useProfile = useRestateScope((scope) => restate(user, { scope }));
const profile = useProfile();
return <p>{profile.name}</p>;
}Wait for a document’s saved data
Saved stores in a scope start restoring after the component commits on the client and are not part of the global restateManager.ready. Wait for the scope’s own manager.ready in a loading boundary.
import { useEffect, useState } from 'react';
import { persistedRestate, useRestateScope } from '@kkwaenggwari/restate';
function Document({ documentId }) {
const { manager, useDraft } = useRestateScope((scope) => ({
manager: scope.manager,
useDraft: persistedRestate('', { scope, namespace: 'documents', key: documentId }),
}));
const [state, setState] = useState('loading');
useEffect(() => {
let active = true;
manager.ready.then(
() => active && setState('ready'),
() => active && setState('error'),
);
return () => {
active = false;
};
}, [manager]);
if (state === 'error') return <p>Could not load this document.</p>;
if (state === 'loading') return <p>Loading document…</p>;
return <Editor useDraft={useDraft} />;
}Try it
Open and close documents and watch their state start fresh.
Preparing the example…
Owners outside React
When a service or server request owns the state, create the scope yourself and call destroy() when finished. Destroying a scope never deletes saved records.
import { createRestateScope, persistedRestate } from '@kkwaenggwari/restate';
export function createDocument(documentId) {
const scope = createRestateScope();
const useDraft = persistedRestate('', {
scope,
namespace: 'documents',
key: documentId,
});
return {
useDraft,
manager: scope.manager,
save: () => scope.manager.saveAll(),
destroy: () => scope.destroy(),
};
}