Стан для React і React Native · 1.0

Спільний стан.Один хук.Готово.

Оголосіть сховище одним рядком і використовуйте його в будь-якому компоненті — без Provider, action і reducer. Компонент перемальовується, лише коли змінюється вибране ним значення, а збереження відбувається, коли вирішуєте ви.

Terminal
npm install @kkwaenggwari/restate

Наживо — справжнє сховище restate

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

Змінюються items і total: перемальовуються три картки, листівка не змінюється.

01 — Просто

Та сама функціональність, менше понять.

Чотири реалізації одного завдання поруч. Числа отримано з показаного тут коду й виміряно під час збирання цього сайту.

Спільний лічильник

Два компоненти читають і оновлюють одне число.

Redux Toolkit Організовує оновлення в slice та action, які надсилаються в сховище, передане через контекст React. Чудові DevTools і суворий, передбачуваний потік оновлень для великих команд.

Zustand Створює хук сховища через create() і змінює його через setState. Крихітний і гнучкий, із middleware для збереження та DevTools.

Jotai Складає атоми й похідні атоми, за бажання всередині Provider. Тонкі залежності між багатьма дрібними шматочками стану.

Redux Toolkit29 рядків · 5 API
JavaScript / JSX
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useDispatch, useSelector } from 'react-redux';

const slice = createSlice({
  name: 'count',
  initialState: 0,
  reducers: { increment: (count) => count + 1 },
});
const store = configureStore({
  reducer: { count: slice.reducer },
});

function Counter() {
  const count = useSelector((state) => state.count);
  const dispatch = useDispatch();
  return <button onClick={() => dispatch(slice.actions.increment())}>{count}</button>;
}

export default function App() {
  return (
    <Provider store={store}>
      <Counter />
      <Counter />
    </Provider>
  );
}
Zustand16 рядків · 1 API
JavaScript / JSX
import { create } from 'zustand';

const useCount = create(() => ({ count: 0 }));
const increment = () =>
  useCount.setState((s) => ({
    count: s.count + 1,
  }));

function Counter() {
  const count = useCount((s) => s.count);
  return <button onClick={increment}>{count}</button>;
}

export default function App() {
  return (
    <>
      <Counter />
      <Counter />
    </>
  );
}
Jotai13 рядків · 2 API
JavaScript / JSX
import { atom, useAtom } from 'jotai';

const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return <button onClick={() => setCount((n) => n + 1)}>{count}</button>;
}

export default function App() {
  return (
    <>
      <Counter />
      <Counter />
    </>
  );
}

restate Одне оголошення — це і сховище, і хук.

restate13 рядків · 1 API
JavaScript / JSX
import { restate } from '@kkwaenggwari/restate';

const useCount = restate(0);

function Counter() {
  const count = useCount();
  return <button onClick={() => useCount.set((n) => n + 1)}>{count}</button>;
}

export default function App() {
  return (
    <>
      <Counter />
      <Counter />
    </>
  );
}

Для цього завдання Zustand і Jotai теж компактні й обходяться без Provider. Розрив зростає, коли додаються збереження та стан для окремих документів.

Вибір одного поля

Компонент заголовка не має перемальовуватися, коли змінюється лише текст.

Redux Toolkit Організовує оновлення в slice та action, які надсилаються в сховище, передане через контекст React. Чудові DevTools і суворий, передбачуваний потік оновлень для великих команд.

Zustand Створює хук сховища через create() і змінює його через setState. Крихітний і гнучкий, із middleware для збереження та DevTools.

Jotai Складає атоми й похідні атоми, за бажання всередині Provider. Тонкі залежності між багатьма дрібними шматочками стану.

Redux Toolkit26 рядків · 4 API
JavaScript / JSX
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector } from 'react-redux';

const article = createSlice({
  name: 'article',
  initialState: { title: 'Hello', body: '' },
  reducers: {
    editBody: (i) => {
      i.body += '!';
    },
    editTitle: (i) => {
      i.title += '!';
    },
  },
});
const store = configureStore({ reducer: article.reducer });

function Title() {
  const title = useSelector((i) => i.title);
  return <h3>{title}</h3>;
}

export default function App() {
  return (
    <Provider store={store}>
      <Title />
      <button onClick={() => store.dispatch(article.actions.editBody())}>body + !</button>
      <button onClick={() => store.dispatch(article.actions.editTitle())}>title + !</button>
    </Provider>
  );
}
Zustand17 рядків · 1 API
JavaScript / JSX
import { create } from 'zustand';

const useArticle = create(() => ({ title: 'Hello', body: '' }));

function Title() {
  const title = useArticle((i) => i.title);
  return <h3>{title}</h3>;
}

export default function App() {
  return (
    <>
      <Title />
      <button
        onClick={() =>
          useArticle.setState((i) => ({
            body: i.body + '!',
          }))
        }
      >
        body + !
      </button>
      <button
        onClick={() =>
          useArticle.setState((i) => ({
            title: i.title + '!',
          }))
        }
      >
        title + !
      </button>
    </>
  );
}
Jotai19 рядків · 3 API
JavaScript / JSX
import { atom, useAtomValue, useSetAtom } from 'jotai';

const articleAtom = atom({ title: 'Hello', body: '' });
const titleAtom = atom((get) => get(articleAtom).title);

function Title() {
  const title = useAtomValue(titleAtom);
  return <h3>{title}</h3>;
}

export default function App() {
  const setArticle = useSetAtom(articleAtom);
  return (
    <>
      <Title />
      <button
        onClick={() =>
          setArticle((i) => ({
            ...i,
            body: i.body + '!',
          }))
        }
      >
        body + !
      </button>
      <button
        onClick={() =>
          setArticle((i) => ({
            ...i,
            title: i.title + '!',
          }))
        }
      >
        title + !
      </button>
    </>
  );
}

restate Передайте selector у той самий хук, яким уже користуєтеся.

restate17 рядків · 1 API
JavaScript / JSX
import { restate } from '@kkwaenggwari/restate';

const useArticle = restate({ title: 'Hello', body: '' });

function Title() {
  const title = useArticle((i) => i.title);
  return <h3>{title}</h3>;
}

export default function App() {
  return (
    <>
      <Title />
      <button
        onClick={() =>
          useArticle.set((i) => ({
            ...i,
            body: i.body + '!',
          }))
        }
      >
        body + !
      </button>
      <button
        onClick={() =>
          useArticle.set((i) => ({
            ...i,
            title: i.title + '!',
          }))
        }
      >
        title + !
      </button>
    </>
  );
}

Вибирати вміють усі інструменти тут. restate використовує той самий виклик хука, а не окремий хук для selector чи похідний атом.

Збереження чернетки

Відновити збережену чернетку під час запуску, зберегти за натисканням і показати стани «зберігається», «не збережено» й «помилка».

Redux Toolkit Організовує оновлення в slice та action, які надсилаються в сховище, передане через контекст React. Чудові DevTools і суворий, передбачуваний потік оновлень для великих команд.

Zustand Створює хук сховища через create() і змінює його через setState. Крихітний і гнучкий, із middleware для збереження та DevTools.

Jotai Складає атоми й похідні атоми, за бажання всередині Provider. Тонкі залежності між багатьма дрібними шматочками стану.

Redux Toolkit84 рядків · 4 API
JavaScript / JSX
import { useEffect, useState } from 'react';
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector } from 'react-redux';

function openDraftDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('draft-example', 1);
    let blocked = false;
    request.onupgradeneeded = () => {
      request.result.createObjectStore('drafts');
    };
    request.onblocked = () => {
      blocked = true;
      reject(new Error('Close other tabs to upgrade storage'));
    };
    request.onerror = () => reject(request.error);
    request.onsuccess = () => {
      const db = request.result;
      if (blocked) {
        db.close();
        return;
      }
      db.onversionchange = () => db.close();
      resolve(db);
    };
  });
}

async function storedDraft(mode, text) {
  const db = await openDraftDB();
  try {
    return await new Promise((resolve, reject) => {
      const tx = db.transaction('drafts', mode);
      const store = tx.objectStore('drafts');
      const request = mode === 'readonly' ? store.get('draft') : store.put(text, 'draft');
      tx.oncomplete = () => resolve(request.result);
      tx.onabort = () => reject(tx.error || new Error('Storage aborted'));
    });
  } finally {
    db.close();
  }
}

const draft = createSlice({
  name: 'draft',
  initialState: '',
  reducers: { edit: (_, action) => action.payload },
});
const store = configureStore({ reducer: { draft: draft.reducer } });
const hydrated = storedDraft('readonly').then((saved) => {
  if (saved === undefined) return;
  if (typeof saved !== 'string') throw new Error('Invalid draft');
  const value = saved;
  store.dispatch(draft.actions.edit(value));
});
// Observe errors immediately, even before the component mounts.
hydrated.catch(() => {});

function DraftEditor() {
  const text = useSelector((state) => state.draft);
  const [ready, setReady] = useState(false);
  const [saving, setSaving] = useState(false);
  const [message, setMessage] = useState('Loading');
  useEffect(() => {
    let active = true;
    hydrated.then(
      () => {
        if (active) {
          setReady(true);
          setMessage('Ready');
        }
      },
      () => {
        if (active) setMessage('Storage error');
      },
    );
    return () => {
      active = false;
    };
  }, []);
  async function save() {
    setSaving(true);
    try {
      await storedDraft('readwrite', store.getState().draft);
      setMessage('Saved');
    } catch {
      setMessage('Save failed');
    } finally {
      setSaving(false);
    }
  }
  return (
    <>
      <label>
        Draft
        <textarea
          value={text}
          disabled={!ready || saving}
          onChange={(event) => {
            const value = event.target.value;
            store.dispatch(draft.actions.edit(value));
            setMessage('Edited');
          }}
        />
      </label>
      <output>{text}</output>
      <button disabled={!ready || saving} onClick={save}>
        Save
      </button>
      <p>{message}</p>
    </>
  );
}

export default function App() {
  return (
    <Provider store={store}>
      <DraftEditor />
    </Provider>
  );
}
Zustand79 рядків · 1 API
JavaScript / JSX
import { useEffect, useState } from 'react';
import { create } from 'zustand';

function openDraftDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('draft-example', 1);
    let blocked = false;
    request.onupgradeneeded = () => {
      request.result.createObjectStore('drafts');
    };
    request.onblocked = () => {
      blocked = true;
      reject(new Error('Close other tabs to upgrade storage'));
    };
    request.onerror = () => reject(request.error);
    request.onsuccess = () => {
      const db = request.result;
      if (blocked) {
        db.close();
        return;
      }
      db.onversionchange = () => db.close();
      resolve(db);
    };
  });
}

async function storedDraft(mode, text) {
  const db = await openDraftDB();
  try {
    return await new Promise((resolve, reject) => {
      const tx = db.transaction('drafts', mode);
      const store = tx.objectStore('drafts');
      const request = mode === 'readonly' ? store.get('draft') : store.put(text, 'draft');
      tx.oncomplete = () => resolve(request.result);
      tx.onabort = () => reject(tx.error || new Error('Storage aborted'));
    });
  } finally {
    db.close();
  }
}

const useDraft = create(() => ({ text: '' }));
const hydrated = storedDraft('readonly').then((saved) => {
  if (saved === undefined) return;
  if (typeof saved !== 'string') throw new Error('Invalid draft');
  const value = saved;
  useDraft.setState({ text: value });
});
// Observe errors immediately, even before the component mounts.
hydrated.catch(() => {});

function DraftEditor() {
  const text = useDraft((state) => state.text);
  const [ready, setReady] = useState(false);
  const [saving, setSaving] = useState(false);
  const [message, setMessage] = useState('Loading');
  useEffect(() => {
    let active = true;
    hydrated.then(
      () => {
        if (active) {
          setReady(true);
          setMessage('Ready');
        }
      },
      () => {
        if (active) setMessage('Storage error');
      },
    );
    return () => {
      active = false;
    };
  }, []);
  async function save() {
    setSaving(true);
    try {
      await storedDraft('readwrite', useDraft.getState().text);
      setMessage('Saved');
    } catch {
      setMessage('Save failed');
    } finally {
      setSaving(false);
    }
  }
  return (
    <>
      <label>
        Draft
        <textarea
          value={text}
          disabled={!ready || saving}
          onChange={(event) => {
            const value = event.target.value;
            useDraft.setState({ text: value });
            setMessage('Edited');
          }}
        />
      </label>
      <output>{text}</output>
      <button disabled={!ready || saving} onClick={save}>
        Save
      </button>
      <p>{message}</p>
    </>
  );
}

export default function App() {
  return <DraftEditor />;
}
Jotai80 рядків · 3 API
JavaScript / JSX
import { useEffect, useState } from 'react';
import { atom, createStore, useAtomValue } from 'jotai';

function openDraftDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('draft-example', 1);
    let blocked = false;
    request.onupgradeneeded = () => {
      request.result.createObjectStore('drafts');
    };
    request.onblocked = () => {
      blocked = true;
      reject(new Error('Close other tabs to upgrade storage'));
    };
    request.onerror = () => reject(request.error);
    request.onsuccess = () => {
      const db = request.result;
      if (blocked) {
        db.close();
        return;
      }
      db.onversionchange = () => db.close();
      resolve(db);
    };
  });
}

async function storedDraft(mode, text) {
  const db = await openDraftDB();
  try {
    return await new Promise((resolve, reject) => {
      const tx = db.transaction('drafts', mode);
      const store = tx.objectStore('drafts');
      const request = mode === 'readonly' ? store.get('draft') : store.put(text, 'draft');
      tx.oncomplete = () => resolve(request.result);
      tx.onabort = () => reject(tx.error || new Error('Storage aborted'));
    });
  } finally {
    db.close();
  }
}

const draftAtom = atom('');
const store = createStore();
const hydrated = storedDraft('readonly').then((saved) => {
  if (saved === undefined) return;
  if (typeof saved !== 'string') throw new Error('Invalid draft');
  const value = saved;
  store.set(draftAtom, value);
});
// Observe errors immediately, even before the component mounts.
hydrated.catch(() => {});

function DraftEditor() {
  const text = useAtomValue(draftAtom, { store });
  const [ready, setReady] = useState(false);
  const [saving, setSaving] = useState(false);
  const [message, setMessage] = useState('Loading');
  useEffect(() => {
    let active = true;
    hydrated.then(
      () => {
        if (active) {
          setReady(true);
          setMessage('Ready');
        }
      },
      () => {
        if (active) setMessage('Storage error');
      },
    );
    return () => {
      active = false;
    };
  }, []);
  async function save() {
    setSaving(true);
    try {
      await storedDraft('readwrite', store.get(draftAtom));
      setMessage('Saved');
    } catch {
      setMessage('Save failed');
    } finally {
      setSaving(false);
    }
  }
  return (
    <>
      <label>
        Draft
        <textarea
          value={text}
          disabled={!ready || saving}
          onChange={(event) => {
            const value = event.target.value;
            store.set(draftAtom, value);
            setMessage('Edited');
          }}
        />
      </label>
      <output>{text}</output>
      <button disabled={!ready || saving} onClick={save}>
        Save
      </button>
      <p>{message}</p>
    </>
  );
}

export default function App() {
  return <DraftEditor />;
}

restate persistedRestate відновлює автоматично, а useStatus повідомляє про збереження, незбережені правки й помилки.

restate18 рядків · 1 API
JavaScript / JSX
import { persistedRestate } from '@kkwaenggwari/restate';

const useDraft = persistedRestate('', {
  key: 'draft',
  validate: (value) => typeof value === 'string',
});

export default function App() {
  const text = useDraft();
  const { dirty, operation, error } = useDraft.useStatus();
  const saving = operation === 'save';
  const message = error ? 'Save failed' : saving ? 'Saving…' : dirty ? 'Edited' : 'Saved';

  return (
    <>
      <label>
        Draft
        <textarea value={text} onChange={(event) => useDraft.set(event.target.value)} />
      </label>
      <output>{text}</output>
      <button disabled={saving} onClick={() => useDraft.save().catch(() => {})}>
        Save
      </button>
      <p>{message}</p>
    </>
  );
}

Інші версії пишуть той самий код IndexedDB вручну. Їхні middleware для збереження чи утиліти сховища можуть скоротити його за допомогою адаптера; цей код тут не враховується.

Стан для окремого документа

Два документи зберігають незалежний стан, а закриття одного звільняє його.

Redux Toolkit Організовує оновлення в slice та action, які надсилаються в сховище, передане через контекст React. Чудові DevTools і суворий, передбачуваний потік оновлень для великих команд.

Zustand Створює хук сховища через create() і змінює його через setState. Крихітний і гнучкий, із middleware для збереження та DevTools.

Jotai Складає атоми й похідні атоми, за бажання всередині Provider. Тонкі залежності між багатьма дрібними шматочками стану.

Redux Toolkit42 рядків · 4 API
JavaScript / JSX
import { useState } from 'react';
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector } from 'react-redux';

const draft = createSlice({
  name: 'draft',
  initialState: '',
  reducers: { edit: (_, action) => action.payload },
});
const selectionSlice = createSlice({
  name: 'selection',
  initialState: 0,
  reducers: { next: (index) => index + 1 },
});
function createDocument() {
  return {
    store: configureStore({
      reducer: {
        draft: draft.reducer,
        selection: selectionSlice.reducer,
      },
    }),
  };
}

function Document({ doc, name }) {
  const text = useSelector((state) => state.draft);
  const selection = useSelector((state) => state.selection);
  return (
    <section>
      <label>
        {name}
        <input
          value={text}
          onChange={(event) => doc.store.dispatch(draft.actions.edit(event.target.value))}
        />
      </label>
      <output>{text}</output>
      <button onClick={() => doc.store.dispatch(selectionSlice.actions.next())}>
        Selection: {selection}
      </button>
    </section>
  );
}

function DocumentSlot({ name }) {
  const [doc] = useState(createDocument);
  return (
    <Provider store={doc.store}>
      <Document doc={doc} name={name} />
    </Provider>
  );
}

export default function App() {
  const [leftOpen, setLeftOpen] = useState(true);
  return (
    <>
      <button onClick={() => setLeftOpen((open) => !open)}>
        {leftOpen ? 'Close left' : 'Open left'}
      </button>
      {leftOpen && <DocumentSlot name="Left" />}
      <DocumentSlot name="Right" />
    </>
  );
}
Zustand35 рядків · 2 API
JavaScript / JSX
import { useState } from 'react';
import { useStore } from 'zustand';
import { createStore } from 'zustand/vanilla';

function createDocument() {
  return {
    draft: createStore(() => ({ text: '' })),
    selection: createStore(() => ({ index: 0 })),
  };
}

function Document({ doc, name }) {
  const text = useStore(doc.draft, (state) => state.text);
  const selection = useStore(doc.selection, (state) => state.index);
  return (
    <section>
      <label>
        {name}
        <input
          value={text}
          onChange={(event) => doc.draft.setState({ text: event.target.value })}
        />
      </label>
      <output>{text}</output>
      <button onClick={() => doc.selection.setState((s) => ({ index: s.index + 1 }))}>
        Selection: {selection}
      </button>
    </section>
  );
}

function DocumentSlot({ name }) {
  const [doc] = useState(createDocument);
  return <Document doc={doc} name={name} />;
}

export default function App() {
  const [leftOpen, setLeftOpen] = useState(true);
  return (
    <>
      <button onClick={() => setLeftOpen((open) => !open)}>
        {leftOpen ? 'Close left' : 'Open left'}
      </button>
      {leftOpen && <DocumentSlot name="Left" />}
      <DocumentSlot name="Right" />
    </>
  );
}
Jotai33 рядків · 3 API
JavaScript / JSX
import { useState } from 'react';
import { atom, createStore, useAtomValue } from 'jotai';

const draftAtom = atom('');
const selectionAtom = atom(0);
function createDocument() {
  return { store: createStore() };
}

function Document({ doc, name }) {
  const text = useAtomValue(draftAtom, { store: doc.store });
  const selection = useAtomValue(selectionAtom, { store: doc.store });
  return (
    <section>
      <label>
        {name}
        <input value={text} onChange={(event) => doc.store.set(draftAtom, event.target.value)} />
      </label>
      <output>{text}</output>
      <button onClick={() => doc.store.set(selectionAtom, (n) => n + 1)}>
        Selection: {selection}
      </button>
    </section>
  );
}

function DocumentSlot({ name }) {
  const [doc] = useState(createDocument);
  return <Document doc={doc} name={name} />;
}

export default function App() {
  const [leftOpen, setLeftOpen] = useState(true);
  return (
    <>
      <button onClick={() => setLeftOpen((open) => !open)}>
        {leftOpen ? 'Close left' : 'Open left'}
      </button>
      {leftOpen && <DocumentSlot name="Left" />}
      <DocumentSlot name="Right" />
    </>
  );
}

restate useRestateScope створює сховища й знищує їх після демонтування.

restate27 рядків · 2 API
JavaScript / JSX
import { useState } from 'react';
import { restate, useRestateScope } from '@kkwaenggwari/restate';

function Document({ name }) {
  const doc = useRestateScope((scope) => ({
    useDraft: restate('', { scope }),
    useSelection: restate(0, { scope }),
  }));
  const text = doc.useDraft();
  const selection = doc.useSelection();
  return (
    <section>
      <label>
        {name}
        <input value={text} onChange={(event) => doc.useDraft.set(event.target.value)} />
      </label>
      <output>{text}</output>
      <button onClick={() => doc.useSelection.set((n) => n + 1)}>Selection: {selection}</button>
    </section>
  );
}

export default function App() {
  const [leftOpen, setLeftOpen] = useState(true);
  return (
    <>
      <button onClick={() => setLeftOpen((open) => !open)}>
        {leftOpen ? 'Close left' : 'Open left'}
      </button>
      {leftOpen && <Document name="Left" />}
      <Document name="Right" />
    </>
  );
}

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, а не поради.

Відкрити лабораторію збоїв Читати посібник із надійності
  1. 01

    Збереження виконуються по черзі

    Кожне сховище виконує операції зі сховищем одну за одною, тож повільне збереження ніколи не дасть старому значенню перезаписати новіше.

    save() · restore()
  2. 02

    Збереження в очікуванні бере найсвіжіше значення

    Виклик save(), поки інше збереження чекає в черзі, приєднується до нього, а не додає нових записів.

    save(): Promise<T>
  3. 03

    Непрочитані записи ніколи не перезаписуються випадково

    Якщо запис не завантажився, autoSave і масові збереження його не чіпають і повідомляють LOAD_FAILED. Замінити його може лише явний save().

    LOAD_FAILED
  4. 04

    Навіть після демонтування збереження завершується

    Збереження, запитані до демонтування, завершуються. Пізніші записи в знищене сховище ігноруються — як стан React після демонтування.

    useRestateScope
  5. 05

    Немає сховища? Застосунок однаково запуститься

    Під час рендерингу на сервері або за заблокованого сховища сховища працюють у пам’яті й повідомляють, що сховище недоступне.

    status().available
  6. 06

    Помилки кажуть, що і де

    Кожна помилка restate містить типізований код і ключ свого запису, щоб ви могли показати потрібне повідомлення.

    error.code · error.key

Росте разом із застосунком

Змінюйте оголошення. Компоненти лишаються ті самі.

Збереження, автозбереження й валідація — це опції того самого сховища. Компонент праворуч не змінюється ніколи.

  1. Пам’ять

    Спільне значення в пам’яті.

    import { restate } from '@kkwaenggwari/restate';
    
    export const useDraft = restate('');
  2. Збережуване

    Відновлюється автоматично; записується, коли ви викликаєте save().

    import { persistedRestate } from '@kkwaenggwari/restate';
    
    export const useDraft = persistedRestate('', { key: 'draft' });
  3. Автозбереження

    Записується один раз після паузи в наборі 500 мс.

    import { persistedRestate } from '@kkwaenggwari/restate';
    
    export const useDraft = persistedRestate('', {
      key: 'draft',
      autoSave: { delayMs: 500 },
    });
  4. З валідацією

    Старі записи мігрують і перевіряються перед використанням.

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

    React

    Працює з React 16.8 і новішими — у браузері й під час рендерингу на сервері.

  • react-native-mmkv >=2.0.0

    React Native

    Ті самі імпорти. Збереження використовує MMKV, щойно ви встановите react-native-mmkv.

  • SSR

    Рендеринг на сервері

    Сховища компонента беруть серверний знімок із props і починають завантаження після гідратації.

  • index.d.ts

    TypeScript

    Оголошення включено. Селектори, поля статусу й коди помилок типізовано.

Початок

Перший спільний стан за хвилину.

Встановіть, оголосіть, використовуйте. Додайте збереження, коли воно знадобиться застосунку.

  1. 01
    Встановлення
    Terminal
    npm install @kkwaenggwari/restate
  2. 02
    Оголосіть сховище
    JavaScript / JSX
    export const useCart = restate({ items: 0, total: 0 });
  3. 03
    Читайте його в будь-якому компоненті
    JavaScript / JSX
    const items = useCart((cart) => cart.items);

KKWAENGGWARI

Проєкти

Пошук у документації

Пошук у заголовках, описах і тексті всіх посібників.