L’état pour React et React Native · 1.0
Partagez l’état.Un hook.C’est tout.
Déclarez un store en une ligne et utilisez-le dans n’importe quel composant, sans Provider, actions ni reducers. Un composant ne se rend que lorsque la valeur qu’il a sélectionnée change, et l’enregistrement a lieu quand vous le décidez.
npm install @kkwaenggwari/restateEn direct — un vrai store restate
useCart.set(cart => ({ ...cart, items: cart.items + 1, total: cart.total + 12 }))items et total changent : trois cartes se rendent, le message ne bouge pas.
- Simple0Provider, actions et reducers à configurerDéclarez une valeur et utilisez-la. Rien à câbler avant.
- Rapide11 → 1Onze modifications, une écritureautoSave attend une pause : onze modifications rapides deviennent une seule écriture.
- Stable26Des codes d’erreur typésChaque échec indique ce qui s’est mal passé et quel enregistrement est concerné.
01 — Simple
La même fonctionnalité, moins de concepts.
Quatre implémentations de la même tâche, côte à côte. Les chiffres proviennent du code affiché ici et sont mesurés lors de la compilation de ce site.
Compteur partagé
Deux composants lisent et mettent à jour un même nombre.
Redux Toolkit Organise les mises à jour en slices et en actions envoyées à un store fourni via le contexte React. D’excellents DevTools et un flux de mise à jour strict et prévisible pour les grandes équipes.
Zustand Crée un hook de store avec create() et le modifie avec setState. Minuscule et flexible, avec des middlewares pour la persistance et les DevTools.
Jotai Compose des atomes et des atomes dérivés, éventuellement dans un Provider. Des dépendances fines entre de nombreux petits morceaux d’état.
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useDispatch, useSelector } from 'react-redux';
const slice = createSlice({
name: 'count',
initialState: 0,
reducers: { increment: (count) => count + 1 },
});
const store = configureStore({
reducer: { count: slice.reducer },
});
function Counter() {
const count = useSelector((state) => state.count);
const dispatch = useDispatch();
return <button onClick={() => dispatch(slice.actions.increment())}>{count}</button>;
}
export default function App() {
return (
<Provider store={store}>
<Counter />
<Counter />
</Provider>
);
}import { create } from 'zustand';
const useCount = create(() => ({ count: 0 }));
const increment = () =>
useCount.setState((s) => ({
count: s.count + 1,
}));
function Counter() {
const count = useCount((s) => s.count);
return <button onClick={increment}>{count}</button>;
}
export default function App() {
return (
<>
<Counter />
<Counter />
</>
);
}import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount((n) => n + 1)}>{count}</button>;
}
export default function App() {
return (
<>
<Counter />
<Counter />
</>
);
}restate Une seule déclaration est à la fois le store et le hook.
import { restate } from '@kkwaenggwari/restate';
const useCount = restate(0);
function Counter() {
const count = useCount();
return <button onClick={() => useCount.set((n) => n + 1)}>{count}</button>;
}
export default function App() {
return (
<>
<Counter />
<Counter />
</>
);
}Pour cette tâche, Zustand et Jotai sont eux aussi légers et sans Provider. L’écart se creuse dès qu’il faut enregistrer et gérer un état par document.
Sélectionner un champ
Le composant du titre ne doit pas se rendre quand seul le corps change.
Redux Toolkit Organise les mises à jour en slices et en actions envoyées à un store fourni via le contexte React. D’excellents DevTools et un flux de mise à jour strict et prévisible pour les grandes équipes.
Zustand Crée un hook de store avec create() et le modifie avec setState. Minuscule et flexible, avec des middlewares pour la persistance et les DevTools.
Jotai Compose des atomes et des atomes dérivés, éventuellement dans un Provider. Des dépendances fines entre de nombreux petits morceaux d’état.
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector } from 'react-redux';
const article = createSlice({
name: 'article',
initialState: { title: 'Hello', body: '' },
reducers: {
editBody: (i) => {
i.body += '!';
},
editTitle: (i) => {
i.title += '!';
},
},
});
const store = configureStore({ reducer: article.reducer });
function Title() {
const title = useSelector((i) => i.title);
return <h3>{title}</h3>;
}
export default function App() {
return (
<Provider store={store}>
<Title />
<button onClick={() => store.dispatch(article.actions.editBody())}>body + !</button>
<button onClick={() => store.dispatch(article.actions.editTitle())}>title + !</button>
</Provider>
);
}import { create } from 'zustand';
const useArticle = create(() => ({ title: 'Hello', body: '' }));
function Title() {
const title = useArticle((i) => i.title);
return <h3>{title}</h3>;
}
export default function App() {
return (
<>
<Title />
<button
onClick={() =>
useArticle.setState((i) => ({
body: i.body + '!',
}))
}
>
body + !
</button>
<button
onClick={() =>
useArticle.setState((i) => ({
title: i.title + '!',
}))
}
>
title + !
</button>
</>
);
}import { atom, useAtomValue, useSetAtom } from 'jotai';
const articleAtom = atom({ title: 'Hello', body: '' });
const titleAtom = atom((get) => get(articleAtom).title);
function Title() {
const title = useAtomValue(titleAtom);
return <h3>{title}</h3>;
}
export default function App() {
const setArticle = useSetAtom(articleAtom);
return (
<>
<Title />
<button
onClick={() =>
setArticle((i) => ({
...i,
body: i.body + '!',
}))
}
>
body + !
</button>
<button
onClick={() =>
setArticle((i) => ({
...i,
title: i.title + '!',
}))
}
>
title + !
</button>
</>
);
}restate Passez un selector au hook que vous utilisez déjà.
import { restate } from '@kkwaenggwari/restate';
const useArticle = restate({ title: 'Hello', body: '' });
function Title() {
const title = useArticle((i) => i.title);
return <h3>{title}</h3>;
}
export default function App() {
return (
<>
<Title />
<button
onClick={() =>
useArticle.set((i) => ({
...i,
body: i.body + '!',
}))
}
>
body + !
</button>
<button
onClick={() =>
useArticle.set((i) => ({
...i,
title: i.title + '!',
}))
}
>
title + !
</button>
</>
);
}Tous ces outils savent sélectionner. restate utilise le même appel au hook plutôt qu’un hook de selector séparé ou un atome dérivé.
Enregistrer un brouillon
Restaurer un brouillon enregistré au démarrage, enregistrer au clic et afficher les états en cours, non enregistré et en échec.
Redux Toolkit Organise les mises à jour en slices et en actions envoyées à un store fourni via le contexte React. D’excellents DevTools et un flux de mise à jour strict et prévisible pour les grandes équipes.
Zustand Crée un hook de store avec create() et le modifie avec setState. Minuscule et flexible, avec des middlewares pour la persistance et les DevTools.
Jotai Compose des atomes et des atomes dérivés, éventuellement dans un Provider. Des dépendances fines entre de nombreux petits morceaux d’état.
import { useEffect, useState } from 'react';
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector } from 'react-redux';
function openDraftDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('draft-example', 1);
let blocked = false;
request.onupgradeneeded = () => {
request.result.createObjectStore('drafts');
};
request.onblocked = () => {
blocked = true;
reject(new Error('Close other tabs to upgrade storage'));
};
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
if (blocked) {
db.close();
return;
}
db.onversionchange = () => db.close();
resolve(db);
};
});
}
async function storedDraft(mode, text) {
const db = await openDraftDB();
try {
return await new Promise((resolve, reject) => {
const tx = db.transaction('drafts', mode);
const store = tx.objectStore('drafts');
const request = mode === 'readonly' ? store.get('draft') : store.put(text, 'draft');
tx.oncomplete = () => resolve(request.result);
tx.onabort = () => reject(tx.error || new Error('Storage aborted'));
});
} finally {
db.close();
}
}
const draft = createSlice({
name: 'draft',
initialState: '',
reducers: { edit: (_, action) => action.payload },
});
const store = configureStore({ reducer: { draft: draft.reducer } });
const hydrated = storedDraft('readonly').then((saved) => {
if (saved === undefined) return;
if (typeof saved !== 'string') throw new Error('Invalid draft');
const value = saved;
store.dispatch(draft.actions.edit(value));
});
// Observe errors immediately, even before the component mounts.
hydrated.catch(() => {});
function DraftEditor() {
const text = useSelector((state) => state.draft);
const [ready, setReady] = useState(false);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState('Loading');
useEffect(() => {
let active = true;
hydrated.then(
() => {
if (active) {
setReady(true);
setMessage('Ready');
}
},
() => {
if (active) setMessage('Storage error');
},
);
return () => {
active = false;
};
}, []);
async function save() {
setSaving(true);
try {
await storedDraft('readwrite', store.getState().draft);
setMessage('Saved');
} catch {
setMessage('Save failed');
} finally {
setSaving(false);
}
}
return (
<>
<label>
Draft
<textarea
value={text}
disabled={!ready || saving}
onChange={(event) => {
const value = event.target.value;
store.dispatch(draft.actions.edit(value));
setMessage('Edited');
}}
/>
</label>
<output>{text}</output>
<button disabled={!ready || saving} onClick={save}>
Save
</button>
<p>{message}</p>
</>
);
}
export default function App() {
return (
<Provider store={store}>
<DraftEditor />
</Provider>
);
}import { useEffect, useState } from 'react';
import { create } from 'zustand';
function openDraftDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('draft-example', 1);
let blocked = false;
request.onupgradeneeded = () => {
request.result.createObjectStore('drafts');
};
request.onblocked = () => {
blocked = true;
reject(new Error('Close other tabs to upgrade storage'));
};
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
if (blocked) {
db.close();
return;
}
db.onversionchange = () => db.close();
resolve(db);
};
});
}
async function storedDraft(mode, text) {
const db = await openDraftDB();
try {
return await new Promise((resolve, reject) => {
const tx = db.transaction('drafts', mode);
const store = tx.objectStore('drafts');
const request = mode === 'readonly' ? store.get('draft') : store.put(text, 'draft');
tx.oncomplete = () => resolve(request.result);
tx.onabort = () => reject(tx.error || new Error('Storage aborted'));
});
} finally {
db.close();
}
}
const useDraft = create(() => ({ text: '' }));
const hydrated = storedDraft('readonly').then((saved) => {
if (saved === undefined) return;
if (typeof saved !== 'string') throw new Error('Invalid draft');
const value = saved;
useDraft.setState({ text: value });
});
// Observe errors immediately, even before the component mounts.
hydrated.catch(() => {});
function DraftEditor() {
const text = useDraft((state) => state.text);
const [ready, setReady] = useState(false);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState('Loading');
useEffect(() => {
let active = true;
hydrated.then(
() => {
if (active) {
setReady(true);
setMessage('Ready');
}
},
() => {
if (active) setMessage('Storage error');
},
);
return () => {
active = false;
};
}, []);
async function save() {
setSaving(true);
try {
await storedDraft('readwrite', useDraft.getState().text);
setMessage('Saved');
} catch {
setMessage('Save failed');
} finally {
setSaving(false);
}
}
return (
<>
<label>
Draft
<textarea
value={text}
disabled={!ready || saving}
onChange={(event) => {
const value = event.target.value;
useDraft.setState({ text: value });
setMessage('Edited');
}}
/>
</label>
<output>{text}</output>
<button disabled={!ready || saving} onClick={save}>
Save
</button>
<p>{message}</p>
</>
);
}
export default function App() {
return <DraftEditor />;
}import { useEffect, useState } from 'react';
import { atom, createStore, useAtomValue } from 'jotai';
function openDraftDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('draft-example', 1);
let blocked = false;
request.onupgradeneeded = () => {
request.result.createObjectStore('drafts');
};
request.onblocked = () => {
blocked = true;
reject(new Error('Close other tabs to upgrade storage'));
};
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
if (blocked) {
db.close();
return;
}
db.onversionchange = () => db.close();
resolve(db);
};
});
}
async function storedDraft(mode, text) {
const db = await openDraftDB();
try {
return await new Promise((resolve, reject) => {
const tx = db.transaction('drafts', mode);
const store = tx.objectStore('drafts');
const request = mode === 'readonly' ? store.get('draft') : store.put(text, 'draft');
tx.oncomplete = () => resolve(request.result);
tx.onabort = () => reject(tx.error || new Error('Storage aborted'));
});
} finally {
db.close();
}
}
const draftAtom = atom('');
const store = createStore();
const hydrated = storedDraft('readonly').then((saved) => {
if (saved === undefined) return;
if (typeof saved !== 'string') throw new Error('Invalid draft');
const value = saved;
store.set(draftAtom, value);
});
// Observe errors immediately, even before the component mounts.
hydrated.catch(() => {});
function DraftEditor() {
const text = useAtomValue(draftAtom, { store });
const [ready, setReady] = useState(false);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState('Loading');
useEffect(() => {
let active = true;
hydrated.then(
() => {
if (active) {
setReady(true);
setMessage('Ready');
}
},
() => {
if (active) setMessage('Storage error');
},
);
return () => {
active = false;
};
}, []);
async function save() {
setSaving(true);
try {
await storedDraft('readwrite', store.get(draftAtom));
setMessage('Saved');
} catch {
setMessage('Save failed');
} finally {
setSaving(false);
}
}
return (
<>
<label>
Draft
<textarea
value={text}
disabled={!ready || saving}
onChange={(event) => {
const value = event.target.value;
store.set(draftAtom, value);
setMessage('Edited');
}}
/>
</label>
<output>{text}</output>
<button disabled={!ready || saving} onClick={save}>
Save
</button>
<p>{message}</p>
</>
);
}
export default function App() {
return <DraftEditor />;
}restate persistedRestate restaure automatiquement, et useStatus signale l’enregistrement en cours, les modifications non enregistrées et les erreurs.
import { persistedRestate } from '@kkwaenggwari/restate';
const useDraft = persistedRestate('', {
key: 'draft',
validate: (value) => typeof value === 'string',
});
export default function App() {
const text = useDraft();
const { dirty, operation, error } = useDraft.useStatus();
const saving = operation === 'save';
const message = error ? 'Save failed' : saving ? 'Saving…' : dirty ? 'Edited' : 'Saved';
return (
<>
<label>
Draft
<textarea value={text} onChange={(event) => useDraft.set(event.target.value)} />
</label>
<output>{text}</output>
<button disabled={saving} onClick={() => useDraft.save().catch(() => {})}>
Save
</button>
<p>{message}</p>
</>
);
}Les autres versions écrivent le même code IndexedDB à la main. Leur middleware de persistance ou leurs utilitaires de stockage peuvent le raccourcir avec un adaptateur ; ce code n’est pas compté ici.
État par document
Deux documents conservent chacun leur état, et fermer l’un le libère.
Redux Toolkit Organise les mises à jour en slices et en actions envoyées à un store fourni via le contexte React. D’excellents DevTools et un flux de mise à jour strict et prévisible pour les grandes équipes.
Zustand Crée un hook de store avec create() et le modifie avec setState. Minuscule et flexible, avec des middlewares pour la persistance et les DevTools.
Jotai Compose des atomes et des atomes dérivés, éventuellement dans un Provider. Des dépendances fines entre de nombreux petits morceaux d’état.
import { useState } from 'react';
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector } from 'react-redux';
const draft = createSlice({
name: 'draft',
initialState: '',
reducers: { edit: (_, action) => action.payload },
});
const selectionSlice = createSlice({
name: 'selection',
initialState: 0,
reducers: { next: (index) => index + 1 },
});
function createDocument() {
return {
store: configureStore({
reducer: {
draft: draft.reducer,
selection: selectionSlice.reducer,
},
}),
};
}
function Document({ doc, name }) {
const text = useSelector((state) => state.draft);
const selection = useSelector((state) => state.selection);
return (
<section>
<label>
{name}
<input
value={text}
onChange={(event) => doc.store.dispatch(draft.actions.edit(event.target.value))}
/>
</label>
<output>{text}</output>
<button onClick={() => doc.store.dispatch(selectionSlice.actions.next())}>
Selection: {selection}
</button>
</section>
);
}
function DocumentSlot({ name }) {
const [doc] = useState(createDocument);
return (
<Provider store={doc.store}>
<Document doc={doc} name={name} />
</Provider>
);
}
export default function App() {
const [leftOpen, setLeftOpen] = useState(true);
return (
<>
<button onClick={() => setLeftOpen((open) => !open)}>
{leftOpen ? 'Close left' : 'Open left'}
</button>
{leftOpen && <DocumentSlot name="Left" />}
<DocumentSlot name="Right" />
</>
);
}import { useState } from 'react';
import { useStore } from 'zustand';
import { createStore } from 'zustand/vanilla';
function createDocument() {
return {
draft: createStore(() => ({ text: '' })),
selection: createStore(() => ({ index: 0 })),
};
}
function Document({ doc, name }) {
const text = useStore(doc.draft, (state) => state.text);
const selection = useStore(doc.selection, (state) => state.index);
return (
<section>
<label>
{name}
<input
value={text}
onChange={(event) => doc.draft.setState({ text: event.target.value })}
/>
</label>
<output>{text}</output>
<button onClick={() => doc.selection.setState((s) => ({ index: s.index + 1 }))}>
Selection: {selection}
</button>
</section>
);
}
function DocumentSlot({ name }) {
const [doc] = useState(createDocument);
return <Document doc={doc} name={name} />;
}
export default function App() {
const [leftOpen, setLeftOpen] = useState(true);
return (
<>
<button onClick={() => setLeftOpen((open) => !open)}>
{leftOpen ? 'Close left' : 'Open left'}
</button>
{leftOpen && <DocumentSlot name="Left" />}
<DocumentSlot name="Right" />
</>
);
}import { useState } from 'react';
import { atom, createStore, useAtomValue } from 'jotai';
const draftAtom = atom('');
const selectionAtom = atom(0);
function createDocument() {
return { store: createStore() };
}
function Document({ doc, name }) {
const text = useAtomValue(draftAtom, { store: doc.store });
const selection = useAtomValue(selectionAtom, { store: doc.store });
return (
<section>
<label>
{name}
<input value={text} onChange={(event) => doc.store.set(draftAtom, event.target.value)} />
</label>
<output>{text}</output>
<button onClick={() => doc.store.set(selectionAtom, (n) => n + 1)}>
Selection: {selection}
</button>
</section>
);
}
function DocumentSlot({ name }) {
const [doc] = useState(createDocument);
return <Document doc={doc} name={name} />;
}
export default function App() {
const [leftOpen, setLeftOpen] = useState(true);
return (
<>
<button onClick={() => setLeftOpen((open) => !open)}>
{leftOpen ? 'Close left' : 'Open left'}
</button>
{leftOpen && <DocumentSlot name="Left" />}
<DocumentSlot name="Right" />
</>
);
}restate useRestateScope crée les stores et les détruit après le démontage.
import { useState } from 'react';
import { restate, useRestateScope } from '@kkwaenggwari/restate';
function Document({ name }) {
const doc = useRestateScope((scope) => ({
useDraft: restate('', { scope }),
useSelection: restate(0, { scope }),
}));
const text = doc.useDraft();
const selection = doc.useSelection();
return (
<section>
<label>
{name}
<input value={text} onChange={(event) => doc.useDraft.set(event.target.value)} />
</label>
<output>{text}</output>
<button onClick={() => doc.useSelection.set((n) => n + 1)}>Selection: {selection}</button>
</section>
);
}
export default function App() {
const [leftOpen, setLeftOpen] = useState(true);
return (
<>
<button onClick={() => setLeftOpen((open) => !open)}>
{leftOpen ? 'Close left' : 'Open left'}
</button>
{leftOpen && <Document name="Left" />}
<Document name="Right" />
</>
);
}Un Provider ou un store Jotai et les vanilla stores de Zustand peuvent aussi isoler l’état ; vous les créez et les connectez vous-même.
Comment ces chiffres sont calculés
Lignes non vides du code affiché, et noms importés depuis chaque bibliothèque d’état (les imports React ne sont pas comptés). Chaque version affiche la même UI avec le même stockage et les mêmes états de chargement et d’erreur. Les exemples restate supposent await restateManager.ready au point d’entrée de l’application. Les sources ont été vérifiées dans la documentation officielle de chaque projet :
- Redux Toolkit — Redux Toolkit · Quick Start, React Redux · useSelector, Redux Toolkit · configureStore. À choisir quand un journal d’actions unique et auditable et un vaste écosystème de middleware comptent avant tout
- Zustand — Zustand · Introduction, Zustand · persist. À choisir quand vous voulez le plus petit noyau possible et préférez composer des middlewares
- Jotai — Jotai · atom, Jotai · Storage, Jotai · Provider. À choisir quand votre état est naturellement un graphe de petites valeurs dérivées
02 — Rapide
Ne rendre que ce qui a changé. N’écrire qu’une fois.
Les deux panneaux exécutent le vrai paquet restate dans votre navigateur. Cliquez, tapez et observez les compteurs.
Carte des rendus
Les mêmes quatre champs, lus de deux façons. Modifiez un champ et comptez les rendus.
- Nom
- Thème
- Compteur
- Brouillon
- Nom
- Thème
- Compteur
- Brouillon
Écritures regroupées
autoSave attend une pause dans la saisie, puis écrit une fois. Une rafale d’appels à save() partage une seule écriture en file d’attente.
persistedRestate('', { key: 'note', autoSave: { delayMs: 400 } });Les chiffres proviennent de vrais rendus React et d’un adaptateur de stockage qui compte les écritures sur cette page. D’autres bibliothèques savent aussi sélectionner des valeurs ; ici, sélectionner est simplement la façon de lire.
03 — Stable
Quand le stockage échoue, vos données restent intactes.
C’est à l’enregistrement que l’état se perd : disques lents, stockage bloqué, onglets fermés. Ce sont des garanties de restate 1.0, pas des conseils.
- 01
Les enregistrements s’exécutent dans l’ordre
Chaque store exécute ses opérations de stockage l’une après l’autre : un enregistrement lent ne laisse jamais une ancienne valeur écraser une plus récente.
save() · restore() - 02
Un enregistrement en attente prend la valeur la plus récente
Appeler save() pendant qu’un autre enregistrement attend dans la file le rejoint au lieu d’empiler des écritures.
save(): Promise<T> - 03
Un enregistrement non lu n’est jamais écrasé par accident
Si un enregistrement n’a pas pu être chargé, autoSave et les enregistrements groupés n’y touchent pas et signalent LOAD_FAILED. Seul un save() explicite peut le remplacer.
LOAD_FAILED - 04
Même après le démontage, l’enregistrement se termine
Les enregistrements demandés avant le démontage se terminent. Les écritures ultérieures dans un store détruit sont ignorées, comme l’état React après démontage.
useRestateScope - 05
Pas de stockage ? L’application démarre quand même
Pendant le rendu serveur ou avec un stockage bloqué, les stores continuent de fonctionner en mémoire et signalent que le stockage est indisponible.
status().available - 06
Les erreurs disent quoi et où
Chaque erreur restate porte un code typé et la clé de son enregistrement, pour que vous puissiez afficher le bon message.
error.code · error.key
Grandit avec votre application
Changez la déclaration. Gardez vos composants.
L’enregistrement, l’enregistrement automatique et la validation sont des options du même store. Le composant de droite ne change jamais.
Mémoire
Une valeur partagée en mémoire.
import { restate } from '@kkwaenggwari/restate'; export const useDraft = restate('');Enregistré
Restauré automatiquement ; écrit quand vous appelez save().
import { persistedRestate } from '@kkwaenggwari/restate'; export const useDraft = persistedRestate('', { key: 'draft' });Enregistrement auto
Écrit une fois après 500 ms de pause dans la saisie.
import { persistedRestate } from '@kkwaenggwari/restate'; export const useDraft = persistedRestate('', { key: 'draft', autoSave: { delayMs: 500 }, });Validé
Les anciens enregistrements sont migrés et vérifiés avant usage.
import { persistedRestate } from '@kkwaenggwari/restate'; export const useDraft = persistedRestate('', { key: 'draft', autoSave: { delayMs: 500 }, version: 2, migrate: (value) => String(value ?? ''), validate: (value) => typeof value === 'string', });
Composant — identique à chaque étape
function Editor() {
const text = useDraft();
return <textarea value={text} onChange={(event) => useDraft.set(event.target.value)} />;
}Partout où React tourne
Un seul paquet pour tous les environnements React.
react >=16.8.0React
Fonctionne avec React 16.8 et versions ultérieures, dans le navigateur et pendant le rendu serveur.
react-native-mmkv >=2.0.0React Native
Les mêmes imports. L’enregistrement utilise MMKV dès que react-native-mmkv est installé.
SSRRendu serveur
Les stores d’un composant prennent leur snapshot serveur dans les props et commencent à charger après l’hydratation.
index.d.tsTypeScript
Déclarations incluses. Les sélecteurs, les champs de statut et les codes d’erreur sont typés.
Bien démarrer
Votre premier état partagé en une minute.
Installez, déclarez, utilisez. Ajoutez l’enregistrement quand votre application en a besoin.
- 01Installer
npm install @kkwaenggwari/restate - 02Déclarer un store
export const useCart = restate({ items: 0, total: 0 }); - 03Le lire dans n’importe quel composant
const items = useCart((cart) => cart.items);
KKWAENGGWARI