Estado para React y React Native · 1.0

Comparte estado.Un hook.Listo.

Declara un store en una línea y úsalo en cualquier componente, sin Provider, acciones ni reducers. Un componente solo se renderiza cuando cambia el valor que seleccionó, y guardar ocurre cuando tú decides.

Terminal
npm install @kkwaenggwari/restate

En vivo — un store real de restate

useCart.set(cart => ({ ...cart, items: cart.items + 1, total: cart.total + 12 }))

Cambian items y total: tres tarjetas se renderizan y la nota no se mueve.

01 — Fácil

La misma función, menos conceptos.

Cuatro implementaciones de la misma tarea, una al lado de otra. Las cifras salen del código que ves aquí y se miden al compilar este sitio.

Contador compartido

Dos componentes leen y actualizan un mismo número.

Redux Toolkit Organiza las actualizaciones en slices y acciones que se envían a un store proporcionado mediante el contexto de React. Excelentes DevTools y un flujo de actualizaciones estricto y predecible para equipos grandes.

Zustand Crea un hook de store con create() y lo modifica con setState. Diminuto y flexible, con middleware para persistencia y DevTools.

Jotai Compone átomos y átomos derivados, opcionalmente dentro de un Provider. Dependencias muy finas entre muchos pequeños fragmentos de estado.

Redux Toolkit29 líneas · 5 APIs
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 líneas · 1 APIs
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 líneas · 2 APIs
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 Una sola declaración es a la vez el store y el hook.

restate13 líneas · 1 APIs
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 />
    </>
  );
}

Para esta tarea, Zustand y Jotai también son pequeños y no necesitan Provider. La diferencia crece cuando entran en juego el guardado y el estado por documento.

Seleccionar un campo

El componente del título no debe renderizarse cuando solo cambia el cuerpo.

Redux Toolkit Organiza las actualizaciones en slices y acciones que se envían a un store proporcionado mediante el contexto de React. Excelentes DevTools y un flujo de actualizaciones estricto y predecible para equipos grandes.

Zustand Crea un hook de store con create() y lo modifica con setState. Diminuto y flexible, con middleware para persistencia y DevTools.

Jotai Compone átomos y átomos derivados, opcionalmente dentro de un Provider. Dependencias muy finas entre muchos pequeños fragmentos de estado.

Redux Toolkit26 líneas · 4 APIs
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 líneas · 1 APIs
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 líneas · 3 APIs
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 Pasa un selector al mismo hook que ya usas.

restate17 líneas · 1 APIs
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>
    </>
  );
}

Todas las herramientas de aquí pueden seleccionar. restate usa la misma llamada al hook en lugar de un hook de selector aparte o un átomo derivado.

Guardar un borrador

Restaurar un borrador guardado al arrancar, guardar al hacer clic y mostrar los estados de guardando, sin guardar y con fallo.

Redux Toolkit Organiza las actualizaciones en slices y acciones que se envían a un store proporcionado mediante el contexto de React. Excelentes DevTools y un flujo de actualizaciones estricto y predecible para equipos grandes.

Zustand Crea un hook de store con create() y lo modifica con setState. Diminuto y flexible, con middleware para persistencia y DevTools.

Jotai Compone átomos y átomos derivados, opcionalmente dentro de un Provider. Dependencias muy finas entre muchos pequeños fragmentos de estado.

Redux Toolkit84 líneas · 4 APIs
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 líneas · 1 APIs
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 líneas · 3 APIs
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 restaura automáticamente, y useStatus informa del guardado, los cambios sin guardar y los errores.

restate18 líneas · 1 APIs
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>
    </>
  );
}

Las otras versiones escriben a mano el mismo código de IndexedDB. Su middleware de persistencia o sus utilidades de almacenamiento pueden acortarlo con un adaptador; ese código no se cuenta aquí.

Estado por documento

Dos documentos mantienen estado independiente, y cerrar uno lo libera.

Redux Toolkit Organiza las actualizaciones en slices y acciones que se envían a un store proporcionado mediante el contexto de React. Excelentes DevTools y un flujo de actualizaciones estricto y predecible para equipos grandes.

Zustand Crea un hook de store con create() y lo modifica con setState. Diminuto y flexible, con middleware para persistencia y DevTools.

Jotai Compone átomos y átomos derivados, opcionalmente dentro de un Provider. Dependencias muy finas entre muchos pequeños fragmentos de estado.

Redux Toolkit42 líneas · 4 APIs
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 líneas · 2 APIs
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 líneas · 3 APIs
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 crea los stores y los destruye tras el desmontaje.

restate27 líneas · 2 APIs
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 o store de Jotai y los vanilla stores de Zustand también pueden aislar estado; los creas y conectas tú.

Cómo se cuentan estas cifras

Líneas no vacías del código mostrado y nombres importados de cada librería de estado (no se cuentan los imports de React). Todas las versiones renderizan la misma UI con el mismo almacenamiento y los mismos estados de carga y error. Los ejemplos de restate dan por hecho await restateManager.ready en la entrada de la app. Las fuentes se contrastaron con la documentación oficial de cada proyecto:

  • Redux Toolkit — Redux Toolkit · Quick Start, React Redux · useSelector, Redux Toolkit · configureStore. Elígelo cuando lo más importante sea un único registro de acciones auditable y un gran ecosistema de middleware
  • Zustand — Zustand · Introduction, Zustand · persist. Elígelo cuando quieras el núcleo más pequeño y prefieras componer middleware
  • Jotai — Jotai · atom, Jotai · Storage, Jotai · Provider. Elígelo cuando tu estado sea, por naturaleza, un grafo de pequeños valores derivados

02 — Rápido

Renderiza solo lo que cambió. Escribe una sola vez.

Ambos paneles ejecutan el paquete real de restate en tu navegador. Haz clic, escribe y mira los contadores.

Mapa de renders

Los mismos cuatro campos, leídos de dos formas. Cambia un campo y cuenta los renders.

Lee el store completo
  • Nombre
  • Tema
  • Contador
  • Borrador
Selecciona un campo
  • Nombre
  • Tema
  • Contador
  • Borrador

Escrituras agrupadas

autoSave espera a que haya una pausa al escribir y luego escribe una vez. Una ráfaga de llamadas a save() comparte una sola escritura en cola.

persistedRestate('', { key: 'note', autoSave: { delayMs: 400 } });

Las cifras vienen de renders reales de React y de un adaptador de almacenamiento que cuenta escrituras en esta página. Otras librerías también pueden seleccionar valores; aquí, seleccionar es simplemente la forma de leer.

03 — Estable

Cuando el almacenamiento falla, tus datos siguen ahí.

Guardar es donde se pierde el estado: discos lentos, almacenamiento bloqueado, pestañas cerradas. Esto son garantías de restate 1.0, no consejos.

Abrir el laboratorio de fallos Leer la guía de fiabilidad
  1. 01

    Los guardados van en orden

    Cada store ejecuta sus operaciones de almacenamiento una tras otra, así que un guardado lento nunca deja que un valor viejo sobrescriba uno nuevo.

    save() · restore()
  2. 02

    Un guardado en espera usa el valor más reciente

    Llamar a save() mientras otro guardado espera en la cola se une a él en lugar de apilar más escrituras.

    save(): Promise<T>
  3. 03

    Los registros sin leer nunca se sobrescriben por accidente

    Si un registro no se pudo cargar, autoSave y los guardados masivos no lo tocan e informan LOAD_FAILED. Solo un save() explícito puede reemplazarlo.

    LOAD_FAILED
  4. 04

    Aunque se desmonte, el guardado termina

    Los guardados pedidos antes del desmontaje se completan. Las escrituras posteriores en un store destruido se ignoran, como el estado de React tras el desmontaje.

    useRestateScope
  5. 05

    ¿Sin almacenamiento? La app arranca igual

    Durante el renderizado en servidor o con el almacenamiento bloqueado, los stores siguen funcionando en memoria e informan de que el almacenamiento no está disponible.

    status().available
  6. 06

    Los errores dicen qué y dónde

    Cada error de restate lleva un código tipado y la clave de su registro, para que puedas mostrar el mensaje adecuado.

    error.code · error.key

Crece con tu app

Cambia la declaración. Conserva tus componentes.

Guardar, guardar automáticamente y validar son opciones del mismo store. El componente de la derecha no cambia nunca.

  1. Memoria

    Un valor compartido en memoria.

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

    Se restaura solo; se escribe cuando llamas a save().

    import { persistedRestate } from '@kkwaenggwari/restate';
    
    export const useDraft = persistedRestate('', { key: 'draft' });
  3. Guardado automático

    Se escribe una vez cuando dejas de escribir durante 500 ms.

    import { persistedRestate } from '@kkwaenggwari/restate';
    
    export const useDraft = persistedRestate('', {
      key: 'draft',
      autoSave: { delayMs: 500 },
    });
  4. Validado

    Los registros antiguos se migran y se comprueban antes de usarse.

    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',
    });

Componente — igual en cada paso

function Editor() {
  const text = useDraft();

  return <textarea value={text} onChange={(event) => useDraft.set(event.target.value)} />;
}

Funciona donde funciona React

Un paquete para cada entorno de React.

  • react >=16.8.0

    React

    Funciona con React 16.8 y posteriores, en el navegador y durante el renderizado en servidor.

  • react-native-mmkv >=2.0.0

    React Native

    Los mismos imports. Para guardar usa MMKV en cuanto instalas react-native-mmkv.

  • SSR

    Renderizado en servidor

    Los stores de un componente toman su snapshot de servidor de las props y empiezan a cargar tras la hidratación.

  • index.d.ts

    TypeScript

    Declaraciones incluidas. Los selectores, los campos de estado y los códigos de error están tipados.

Empezar

Tu primer estado compartido en un minuto.

Instala, declara, usa. Añade el guardado cuando tu app lo necesite.

  1. 01
    Instalar
    Terminal
    npm install @kkwaenggwari/restate
  2. 02
    Declarar un store
    JavaScript / JSX
    export const useCart = restate({ items: 0, total: 0 });
  3. 03
    Leerlo en cualquier componente
    JavaScript / JSX
    const items = useCart((cart) => cart.items);

KKWAENGGWARI

Proyectos

Buscar en la documentación

Busca en títulos, descripciones y texto de todas las guías.