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.

Terminal
npm install @kkwaenggwari/restate

En 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.

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.

Redux Toolkit29 lignes · 5 API
JavaScript / JSX
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>
  );
}
Zustand16 lignes · 1 API
JavaScript / JSX
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 />
    </>
  );
}
Jotai13 lignes · 2 API
JavaScript / JSX
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.

restate13 lignes · 1 API
JavaScript / JSX
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.

Redux Toolkit26 lignes · 4 API
JavaScript / JSX
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>
  );
}
Zustand17 lignes · 1 API
JavaScript / JSX
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>
    </>
  );
}
Jotai19 lignes · 3 API
JavaScript / JSX
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à.

restate17 lignes · 1 API
JavaScript / JSX
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.

Redux Toolkit84 lignes · 4 API
JavaScript / JSX
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>
  );
}
Zustand79 lignes · 1 API
JavaScript / JSX
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 />;
}
Jotai80 lignes · 3 API
JavaScript / JSX
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.

restate18 lignes · 1 API
JavaScript / JSX
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.

Redux Toolkit42 lignes · 4 API
JavaScript / JSX
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" />
    </>
  );
}
Zustand35 lignes · 2 API
JavaScript / JSX
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" />
    </>
  );
}
Jotai33 lignes · 3 API
JavaScript / JSX
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.

restate27 lignes · 2 API
JavaScript / JSX
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.

Lit tout le store
  • Nom
  • Thème
  • Compteur
  • Brouillon
Sélectionne un champ
  • 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.

Ouvrir le labo des pannes Lire le guide de fiabilité
  1. 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()
  2. 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>
  3. 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
  4. 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
  5. 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
  6. 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.

  1. Mémoire

    Une valeur partagée en mémoire.

    import { restate } from '@kkwaenggwari/restate';
    
    export const useDraft = restate('');
  2. Enregistré

    Restauré automatiquement ; écrit quand vous appelez save().

    import { persistedRestate } from '@kkwaenggwari/restate';
    
    export const useDraft = persistedRestate('', { key: 'draft' });
  3. 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 },
    });
  4. 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.0

    React

    Fonctionne avec React 16.8 et versions ultérieures, dans le navigateur et pendant le rendu serveur.

  • react-native-mmkv >=2.0.0

    React Native

    Les mêmes imports. L’enregistrement utilise MMKV dès que react-native-mmkv est installé.

  • SSR

    Rendu serveur

    Les stores d’un composant prennent leur snapshot serveur dans les props et commencent à charger après l’hydratation.

  • index.d.ts

    TypeScript

    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.

  1. 01
    Installer
    Terminal
    npm install @kkwaenggwari/restate
  2. 02
    Déclarer un store
    JavaScript / JSX
    export const useCart = restate({ items: 0, total: 0 });
  3. 03
    Le lire dans n’importe quel composant
    JavaScript / JSX
    const items = useCart((cart) => cart.items);

KKWAENGGWARI

Projets

Rechercher dans la documentation

Recherche dans les titres, les descriptions et le texte de tous les guides.