Состояние для 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

Проекты

Поиск по документации

Поиск по заголовкам, описаниям и тексту всех руководств.