Состояние для React и React Native · 1.0
Общее состояние.Один хук.Готово.
Объявите хранилище одной строкой и используйте его в любом компоненте — без Provider, action и reducer. Компонент перерисовывается, только когда меняется выбранное им значение, а сохранение происходит, когда решаете вы.
npm install @kkwaenggwari/restateВживую — настоящее хранилище restate
useCart.set(cart => ({ ...cart, items: cart.items + 1, total: cart.total + 12 }))Меняются items и total: перерисовываются три карточки, открытка не трогается.
- Просто0Provider, action и reducer, которые надо настраиватьОбъявите значение и используйте. Заранее ничего подключать не нужно.
- Быстро11 → 1Одиннадцать правок, одна записьautoSave ждёт паузы, поэтому одиннадцать быстрых правок превращаются в одну запись.
- Надёжно26Типизированные коды ошибокКаждая ошибка говорит, что пошло не так и какую запись это затронуло.
01 — Просто
Та же функциональность, меньше понятий.
Четыре реализации одной задачи рядом. Цифры получены из показанного здесь кода и измеряются при сборке этого сайта.
Задача Общий счётчик · Выбор одного поля · Сохранение черновика · Состояние для отдельного документа
Общий счётчик
Два компонента читают и обновляют одно и то же число.
Redux Toolkit Организует обновления в slice и action, которые отправляются в хранилище, переданное через контекст React. Отличные DevTools и строгий, предсказуемый поток обновлений для больших команд.
Zustand Создаёт хук хранилища через create() и меняет его через setState. Крошечный и гибкий, с middleware для сохранения и DevTools.
Jotai Собирает атомы и производные атомы, при желании внутри Provider. Тонкие зависимости между множеством мелких кусочков состояния.
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 Одно объявление — это и хранилище, и хук.
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 />
</>
);
}Для этой задачи Zustand и Jotai тоже компактны и обходятся без Provider. Разрыв растёт, когда добавляются сохранение и состояние для отдельных документов.
Выбор одного поля
Компонент заголовка не должен перерисовываться, когда меняется только текст.
Redux Toolkit Организует обновления в slice и action, которые отправляются в хранилище, переданное через контекст React. Отличные DevTools и строгий, предсказуемый поток обновлений для больших команд.
Zustand Создаёт хук хранилища через create() и меняет его через setState. Крошечный и гибкий, с middleware для сохранения и DevTools.
Jotai Собирает атомы и производные атомы, при желании внутри Provider. Тонкие зависимости между множеством мелких кусочков состояния.
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 Передайте selector в тот же хук, который уже используете.
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>
</>
);
}Выбирать умеют все инструменты здесь. restate использует тот же вызов хука, а не отдельный хук для selector или производный атом.
Сохранение черновика
Восстановить сохранённый черновик при запуске, сохранить по нажатию и показать состояния «сохраняется», «не сохранено» и «ошибка».
Redux Toolkit Организует обновления в slice и action, которые отправляются в хранилище, переданное через контекст React. Отличные DevTools и строгий, предсказуемый поток обновлений для больших команд.
Zustand Создаёт хук хранилища через create() и меняет его через setState. Крошечный и гибкий, с middleware для сохранения и DevTools.
Jotai Собирает атомы и производные атомы, при желании внутри Provider. Тонкие зависимости между множеством мелких кусочков состояния.
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 восстанавливает автоматически, а useStatus сообщает о сохранении, несохранённых правках и ошибках.
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>
</>
);
}Другие версии пишут тот же код IndexedDB вручную. Их middleware для сохранения или утилиты хранилища могут сократить его с помощью адаптера; этот код здесь не учитывается.
Состояние для отдельного документа
Два документа хранят независимое состояние, а закрытие одного освобождает его.
Redux Toolkit Организует обновления в slice и action, которые отправляются в хранилище, переданное через контекст React. Отличные DevTools и строгий, предсказуемый поток обновлений для больших команд.
Zustand Создаёт хук хранилища через create() и меняет его через setState. Крошечный и гибкий, с middleware для сохранения и DevTools.
Jotai Собирает атомы и производные атомы, при желании внутри Provider. Тонкие зависимости между множеством мелких кусочков состояния.
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 создаёт хранилища и уничтожает их после размонтирования.
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" />
</>
);
}Provider или store в Jotai и vanilla-хранилища Zustand тоже могут изолировать состояние; создавать и подключать их приходится вам.
Как считаются эти цифры
Непустые строки показанного кода и имена, импортированные из каждой библиотеки состояния (импорты React не считаются). Каждая версия рисует тот же UI с тем же хранилищем и теми же состояниями загрузки и ошибки. Примеры restate предполагают await restateManager.ready в точке входа приложения. Источники сверены с официальной документацией каждого проекта:
- Redux Toolkit — Redux Toolkit · Quick Start, React Redux · useSelector, Redux Toolkit · configureStore. Выбирайте его, когда важнее всего единый проверяемый журнал action и большая экосистема middleware
- Zustand — Zustand · Introduction, Zustand · persist. Выбирайте его, когда нужно минимальное ядро и вы предпочитаете собирать middleware
- Jotai — Jotai · atom, Jotai · Storage, Jotai · Provider. Выбирайте его, когда ваше состояние по природе — граф маленьких производных значений
02 — Быстро
Перерисовывается только изменившееся. Запись — один раз.
Обе панели запускают настоящий пакет restate в вашем браузере. Нажимайте, печатайте и следите за счётчиками.
Карта рендеров
Одни и те же четыре поля, прочитанные двумя способами. Измените поле и посчитайте рендеры.
- Имя
- Тема
- Счётчик
- Черновик
- Имя
- Тема
- Счётчик
- Черновик
Объединённые записи
autoSave ждёт паузы в наборе текста и затем пишет один раз. Серия вызовов save() делит одну запись в очереди.
persistedRestate('', { key: 'note', autoSave: { delayMs: 400 } });Цифры получены из настоящих рендеров React и адаптера хранилища, который считает записи на этой странице. Другие библиотеки тоже умеют выбирать значения; здесь выборка — просто способ чтения.
03 — Надёжно
Когда хранилище даёт сбой, ваши данные остаются на месте.
Состояние теряется именно при сохранении: медленные диски, заблокированное хранилище, закрытые вкладки. Это гарантии restate 1.0, а не советы.
- 01
Сохранения выполняются по порядку
Каждое хранилище выполняет операции с хранилищем одну за другой, поэтому медленное сохранение никогда не даст старому значению перезаписать более новое.
save() · restore() - 02
Ожидающее сохранение берёт самое свежее значение
Вызов save(), пока другое сохранение ждёт в очереди, присоединяется к нему, а не добавляет новых записей.
save(): Promise<T> - 03
Непрочитанные записи никогда не перезаписываются случайно
Если запись не загрузилась, autoSave и массовые сохранения её не трогают и сообщают LOAD_FAILED. Заменить её может только явный save().
LOAD_FAILED - 04
Даже после размонтирования сохранение завершается
Сохранения, запрошенные до размонтирования, завершаются. Последующие записи в уничтоженное хранилище игнорируются — как состояние React после размонтирования.
useRestateScope - 05
Нет хранилища? Приложение всё равно запустится
При рендеринге на сервере или при заблокированном хранилище хранилища продолжают работать в памяти и сообщают, что хранилище недоступно.
status().available - 06
Ошибки говорят, что и где
Каждая ошибка restate несёт типизированный код и ключ своей записи, чтобы вы могли показать нужное сообщение.
error.code · error.key
Растёт вместе с приложением
Меняйте объявление. Компоненты остаются прежними.
Сохранение, автосохранение и валидация — это опции того же хранилища. Компонент справа не меняется никогда.
Память
Общее значение в памяти.
import { restate } from '@kkwaenggwari/restate'; export const useDraft = restate('');Сохраняемое
Восстанавливается автоматически; записывается, когда вы вызываете save().
import { persistedRestate } from '@kkwaenggwari/restate'; export const useDraft = persistedRestate('', { key: 'draft' });Автосохранение
Записывается один раз после паузы в наборе 500 мс.
import { persistedRestate } from '@kkwaenggwari/restate'; export const useDraft = persistedRestate('', { key: 'draft', autoSave: { delayMs: 500 }, });С валидацией
Старые записи мигрируют и проверяются перед использованием.
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', });
Компонент — одинаковый на каждом шаге
function Editor() {
const text = useDraft();
return <textarea value={text} onChange={(event) => useDraft.set(event.target.value)} />;
}Работает везде, где работает React
Один пакет для любой среды React.
react >=16.8.0React
Работает с React 16.8 и новее — в браузере и при рендеринге на сервере.
react-native-mmkv >=2.0.0React Native
Те же импорты. Сохранение использует MMKV, как только вы установите react-native-mmkv.
SSRРендеринг на сервере
Хранилища компонента берут серверный снимок из props и начинают загрузку после гидратации.
index.d.tsTypeScript
Объявления включены. Селекторы, поля статуса и коды ошибок типизированы.
Начало
Первое общее состояние за минуту.
Установите, объявите, используйте. Добавьте сохранение, когда оно понадобится приложению.
- 01Установка
npm install @kkwaenggwari/restate - 02Объявите хранилище
export const useCart = restate({ items: 0, total: 0 }); - 03Читайте его в любом компоненте
const items = useCart((cart) => cart.items);
KKWAENGGWARI