Състояние за React и React Native · 1.0
Споделете състояние.Един hook.Готово.
Декларирайте store на един ред и го използвайте във всеки компонент, без Provider, action или reducer. Компонентът се пререндерира само когато избраната от него стойност се промени, а запазването става, когато решите.
npm install @kkwaenggwari/restateНа живо — истински store на 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, изпратени до store, предоставен чрез контекста на React. Отлични DevTools и строг, предвидим поток на обновяване за големи екипи.
Zustand Създава hook за store с 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 Една декларация е едновременно store и 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 />
</>
);
}За тази задача Zustand и Jotai също са малки и без Provider. Разликата расте, когато се добавят запазване и състояние за всеки документ.
Избор на поле
Компонентът на заглавието не бива да се пререндерира, когато се промени само текстът.
Redux Toolkit Организира обновяванията в slice и action, изпратени до store, предоставен чрез контекста на React. Отлични DevTools и строг, предвидим поток на обновяване за големи екипи.
Zustand Създава hook за store с 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 на същия hook, който вече използвате.
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 използва същото извикване на hook вместо отделен hook за selector или производен атом.
Запазване на чернова
Възстановете запазена чернова при стартиране, запазете при клик и покажете състоянията запазва се, незапазено и неуспех.
Redux Toolkit Организира обновяванията в slice и action, изпратени до store, предоставен чрез контекста на React. Отлични DevTools и строг, предвидим поток на обновяване за големи екипи.
Zustand Създава hook за store с 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, изпратени до store, предоставен чрез контекста на React. Отлични DevTools и строг, предвидим поток на обновяване за големи екипи.
Zustand Създава hook за store с 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 създава store и ги унищожава след демонтиране.
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 store в 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
Запазванията стават по ред
Всеки store изпълнява операциите с хранилището една след друга, така че бавно запазване никога не позволява стара стойност да презапише по-нова.
save() · restore() - 02
Чакащото запазване взема най-новата стойност
Извикване на save(), докато друго запазване чака в опашката, се присъединява към него, вместо да трупа нови записи.
save(): Promise<T> - 03
Непрочетени записи никога не се презаписват по случайност
Ако запис не се е заредил, autoSave и груповите запазвания не го докосват и съобщават LOAD_FAILED. Само изрично save() може да го замени.
LOAD_FAILED - 04
Дори след демонтиране запазването завършва
Запазванията, поискани преди демонтиране, завършват. По-късни записи в унищожен store се игнорират, точно както състоянието на React след демонтиране.
useRestateScope - 05
Няма хранилище? Приложението стартира въпреки това
При рендериране на сървъра или при блокирано хранилище store продължават да работят в паметта и съобщават, че хранилището не е налично.
status().available - 06
Грешките казват какво и къде
Всяка грешка на restate носи типизиран код и ключа на своя запис, за да покажете правилното съобщение.
error.code · error.key
Расте заедно с приложението
Сменете декларацията. Запазете компонентите.
Запазване, автоматично запазване и валидация са опции на един и същ store. Компонентът вдясно никога не се променя.
Памет
Споделена стойност в паметта.
import { restate } from '@kkwaenggwari/restate'; export const useDraft = restate('');Запазван
Възстановява се автоматично; записва се, когато извикате save().
import { persistedRestate } from '@kkwaenggwari/restate'; export const useDraft = persistedRestate('', { key: 'draft' });Автоматично запазване
Записва се веднъж след 500 ms пауза в писането.
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Рендериране на сървъра
Store на компонента вземат моментната снимка от сървъра от props и започват зареждане след хидратация.
index.d.tsTypeScript
Декларациите са включени. Селекторите, полетата на статуса и кодовете на грешки са типизирани.
Начало
Първото ви споделено състояние за минута.
Инсталирайте, декларирайте, използвайте. Добавете запазване, когато приложението ви има нужда.
- 01Инсталиране
npm install @kkwaenggwari/restate - 02Декларирайте store
export const useCart = restate({ items: 0, total: 0 }); - 03Четете го във всеки компонент
const items = useCart((cart) => cart.items);
KKWAENGGWARI