Stan dla React i React Native · 1.0

Wspólny stan.Jeden hook.Gotowe.

Zadeklaruj magazyn w jednej linii i używaj go w dowolnym komponencie, bez Providera, akcji i reducerów. Komponent renderuje się ponownie tylko wtedy, gdy zmienia się wartość, którą wybrał, a zapis następuje, kiedy zdecydujesz.

Terminal
npm install @kkwaenggwari/restate

Na żywo — prawdziwy magazyn restate

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

Zmieniają się items i total: renderują się trzy karty, kartka zostaje bez zmian.

01 — Prosto

Ta sama funkcja, mniej pojęć.

Cztery implementacje tego samego zadania obok siebie. Liczby pochodzą z pokazanego tu kodu i są mierzone podczas budowania tej strony.

Wspólny licznik

Dwa komponenty odczytują i aktualizują tę samą liczbę.

Redux Toolkit Organizuje aktualizacje w slice i akcje wysyłane do magazynu dostarczonego przez kontekst React. Świetne DevTools i ścisły, przewidywalny przepływ aktualizacji dla dużych zespołów.

Zustand Tworzy hook magazynu przez create() i zmienia go przez setState. Malutki i elastyczny, z middleware do zapisu i DevTools.

Jotai Składa atomy i atomy pochodne, opcjonalnie wewnątrz Providera. Precyzyjne zależności między wieloma małymi kawałkami stanu.

Redux Toolkit29 linie · 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 linie · 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 linie · 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 Jedna deklaracja jest jednocześnie magazynem i hookiem.

restate13 linie · 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 />
    </>
  );
}

W tym zadaniu Zustand i Jotai też są małe i obchodzą się bez Providera. Różnica rośnie, gdy dochodzą zapis i stan dla każdego dokumentu.

Wybierz pole

Komponent tytułu nie powinien się renderować, gdy zmienia się tylko tekst.

Redux Toolkit Organizuje aktualizacje w slice i akcje wysyłane do magazynu dostarczonego przez kontekst React. Świetne DevTools i ścisły, przewidywalny przepływ aktualizacji dla dużych zespołów.

Zustand Tworzy hook magazynu przez create() i zmienia go przez setState. Malutki i elastyczny, z middleware do zapisu i DevTools.

Jotai Składa atomy i atomy pochodne, opcjonalnie wewnątrz Providera. Precyzyjne zależności między wieloma małymi kawałkami stanu.

Redux Toolkit26 linie · 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 linie · 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 linie · 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 Przekaż selector do tego samego hooka, którego już używasz.

restate17 linie · 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>
    </>
  );
}

Wszystkie narzędzia tutaj potrafią wybierać. restate używa tego samego wywołania hooka zamiast osobnego hooka dla selector czy pochodnego atomu.

Zapisz szkic

Przywróć zapisany szkic przy starcie, zapisz po kliknięciu i pokaż stany zapisywania, braku zapisu i błędu.

Redux Toolkit Organizuje aktualizacje w slice i akcje wysyłane do magazynu dostarczonego przez kontekst React. Świetne DevTools i ścisły, przewidywalny przepływ aktualizacji dla dużych zespołów.

Zustand Tworzy hook magazynu przez create() i zmienia go przez setState. Malutki i elastyczny, z middleware do zapisu i DevTools.

Jotai Składa atomy i atomy pochodne, opcjonalnie wewnątrz Providera. Precyzyjne zależności między wieloma małymi kawałkami stanu.

Redux Toolkit84 linie · 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 linie · 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 linie · 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 przywraca automatycznie, a useStatus raportuje zapis, niezapisane edycje i błędy.

restate18 linie · 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>
    </>
  );
}

Pozostałe wersje piszą ten sam kod IndexedDB ręcznie. Ich middleware do zapisu lub narzędzia pamięci mogą go skrócić adapterem; tego kodu tu nie liczymy.

Stan dla każdego dokumentu

Dwa dokumenty mają niezależny stan, a zamknięcie jednego go zwalnia.

Redux Toolkit Organizuje aktualizacje w slice i akcje wysyłane do magazynu dostarczonego przez kontekst React. Świetne DevTools i ścisły, przewidywalny przepływ aktualizacji dla dużych zespołów.

Zustand Tworzy hook magazynu przez create() i zmienia go przez setState. Malutki i elastyczny, z middleware do zapisu i DevTools.

Jotai Składa atomy i atomy pochodne, opcjonalnie wewnątrz Providera. Precyzyjne zależności między wieloma małymi kawałkami stanu.

Redux Toolkit42 linie · 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 linie · 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 linie · 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 tworzy magazyny i niszczy je po odmontowaniu.

restate27 linie · 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 lub store w Jotai i magazyny vanilla w Zustand też mogą izolować stan; tworzysz je i podłączasz samodzielnie.

Jak liczone są te liczby

Niepuste linie pokazanego kodu i nazwy importowane z każdej biblioteki stanu (importy React się nie liczą). Każda wersja renderuje ten sam UI z tą samą pamięcią oraz tymi samymi stanami ładowania i błędu. Przykłady restate zakładają await restateManager.ready w punkcie wejścia aplikacji. Źródła sprawdzono z oficjalną dokumentacją każdego projektu:

  • Redux Toolkit — Redux Toolkit · Quick Start, React Redux · useSelector, Redux Toolkit · configureStore. Wybierz go, gdy najważniejsze są jeden audytowalny dziennik akcji i duży ekosystem middleware
  • Zustand — Zustand · Introduction, Zustand · persist. Wybierz go, gdy chcesz najmniejszego rdzenia i wolisz składać middleware
  • Jotai — Jotai · atom, Jotai · Storage, Jotai · Provider. Wybierz go, gdy twój stan jest z natury grafem małych wartości pochodnych

02 — Szybko

Renderuj tylko to, co się zmieniło. Zapisuj raz.

Oba panele uruchamiają prawdziwy pakiet restate w twojej przeglądarce. Klikaj, pisz i obserwuj liczniki.

Mapa renderowań

Te same cztery pola odczytywane na dwa sposoby. Zmień pole i policz renderowania.

Odczytuje cały magazyn
  • Nazwa
  • Motyw
  • Licznik
  • Szkic
Wybiera jedno pole
  • Nazwa
  • Motyw
  • Licznik
  • Szkic

Połączone zapisy

autoSave czeka na przerwę w pisaniu, a potem zapisuje raz. Seria wywołań save() dzieli jeden zapis w kolejce.

persistedRestate('', { key: 'note', autoSave: { delayMs: 400 } });

Liczby pochodzą z prawdziwych renderowań React i adaptera pamięci, który liczy zapisy na tej stronie. Inne biblioteki też potrafią wybierać wartości; tutaj wybór jest po prostu sposobem odczytu.

03 — Stabilnie

Gdy pamięć zawodzi, twoje dane zostają na miejscu.

Stan ginie właśnie przy zapisie: wolne dyski, zablokowana pamięć, zamknięte karty. To gwarancje restate 1.0, a nie porady.

Otwórz laboratorium awarii Przeczytaj przewodnik o niezawodności
  1. 01

    Zapisy wykonują się po kolei

    Każdy magazyn wykonuje operacje na pamięci jedna po drugiej, więc wolny zapis nigdy nie pozwoli starej wartości nadpisać nowszej.

    save() · restore()
  2. 02

    Oczekujący zapis bierze najnowszą wartość

    Wywołanie save(), gdy inny zapis czeka w kolejce, dołącza do niego zamiast dokładać kolejne zapisy.

    save(): Promise<T>
  3. 03

    Nieodczytane rekordy nigdy nie zostaną przypadkiem nadpisane

    Jeśli rekord się nie wczytał, autoSave i zapisy zbiorcze go nie ruszają i zgłaszają LOAD_FAILED. Zastąpić go może tylko jawne save().

    LOAD_FAILED
  4. 04

    Nawet po odmontowaniu zapis się kończy

    Zapisy zlecone przed odmontowaniem są kończone. Późniejsze zapisy do zniszczonego magazynu są ignorowane, jak stan React po odmontowaniu.

    useRestateScope
  5. 05

    Brak pamięci? Aplikacja i tak wystartuje

    Podczas renderowania na serwerze lub przy zablokowanej pamięci magazyny działają dalej w pamięci operacyjnej i zgłaszają, że pamięć trwała jest niedostępna.

    status().available
  6. 06

    Błędy mówią co i gdzie

    Każdy błąd restate niesie typowany kod i klucz swojego rekordu, więc możesz pokazać właściwy komunikat.

    error.code · error.key

Rośnie razem z aplikacją

Zmień deklarację. Zachowaj komponenty.

Zapis, autozapis i walidacja to opcje tego samego magazynu. Komponent po prawej nigdy się nie zmienia.

  1. Pamięć

    Wspólna wartość w pamięci.

    import { restate } from '@kkwaenggwari/restate';
    
    export const useDraft = restate('');
  2. Zapisywany

    Przywracany automatycznie; zapisywany, gdy wywołasz save().

    import { persistedRestate } from '@kkwaenggwari/restate';
    
    export const useDraft = persistedRestate('', { key: 'draft' });
  3. Autozapis

    Zapisywany raz po 500 ms przerwy w pisaniu.

    import { persistedRestate } from '@kkwaenggwari/restate';
    
    export const useDraft = persistedRestate('', {
      key: 'draft',
      autoSave: { delayMs: 500 },
    });
  4. Z walidacją

    Starsze rekordy są migrowane i sprawdzane przed użyciem.

    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',
    });

Komponent — taki sam na każdym etapie

function Editor() {
  const text = useDraft();

  return <textarea value={text} onChange={(event) => useDraft.set(event.target.value)} />;
}

Działa tam, gdzie działa React

Jeden pakiet dla każdego środowiska React.

  • react >=16.8.0

    React

    Działa z React 16.8 i nowszym, w przeglądarce i podczas renderowania na serwerze.

  • react-native-mmkv >=2.0.0

    React Native

    Te same importy. Zapis używa MMKV, gdy tylko zainstalujesz react-native-mmkv.

  • SSR

    Renderowanie na serwerze

    Magazyny komponentu biorą migawkę serwera z props i zaczynają wczytywanie po hydratacji.

  • index.d.ts

    TypeScript

    Deklaracje w zestawie. Selektory, pola statusu i kody błędów są typowane.

Zacznij

Twój pierwszy wspólny stan w minutę.

Zainstaluj, zadeklaruj, używaj. Dodaj zapis, gdy aplikacja będzie go potrzebować.

  1. 01
    Instalacja
    Terminal
    npm install @kkwaenggwari/restate
  2. 02
    Zadeklaruj magazyn
    JavaScript / JSX
    export const useCart = restate({ items: 0, total: 0 });
  3. 03
    Odczytuj go w dowolnym komponencie
    JavaScript / JSX
    const items = useCart((cart) => cart.items);

KKWAENGGWARI

Projekty

Szukaj w dokumentacji

Przeszukuj tytuły, opisy i treść wszystkich przewodników.