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.
npm install @kkwaenggwari/restateEn 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.
- Fácil0Provider, acciones y reducers que configurarDeclara un valor y úsalo. No hay nada que conectar antes.
- Rápido11 → 1Once ediciones, una escrituraautoSave espera a una pausa, así que once ediciones rápidas se convierten en una sola escritura.
- Estable26Códigos de error tipadosCada fallo dice qué salió mal y en qué registro ocurrió.
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.
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 Una sola declaración es a la vez el store y el 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 />
</>
);
}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.
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 Pasa un selector al mismo hook que ya usas.
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.
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 restaura automáticamente, y useStatus informa del guardado, los cambios sin guardar y los errores.
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.
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 crea los stores y los destruye tras el desmontaje.
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.
- Nombre
- Tema
- Contador
- Borrador
- 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.
- 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() - 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> - 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 - 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 - 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 - 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.
Memoria
Un valor compartido en memoria.
import { restate } from '@kkwaenggwari/restate'; export const useDraft = restate('');Guardado
Se restaura solo; se escribe cuando llamas a save().
import { persistedRestate } from '@kkwaenggwari/restate'; export const useDraft = persistedRestate('', { key: 'draft' });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 }, });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.0React
Funciona con React 16.8 y posteriores, en el navegador y durante el renderizado en servidor.
react-native-mmkv >=2.0.0React Native
Los mismos imports. Para guardar usa MMKV en cuanto instalas react-native-mmkv.
SSRRenderizado 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.tsTypeScript
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.
- 01Instalar
npm install @kkwaenggwari/restate - 02Declarar un store
export const useCart = restate({ items: 0, total: 0 }); - 03Leerlo en cualquier componente
const items = useCart((cart) => cart.items);
KKWAENGGWARI