Skip to content

t8js/react-store

Repository files navigation

npm Lightweight TypeScript ✓ CSR ✓ SSR ✓

@t8/react-store

Concise shared state management for React apps

  • Similar to useState()
  • No boilerplate
  • Painless transition from local state to shared state and vice versa
  • SSR-compatible

Installation: npm i @t8/react-store

Shared state setup

Moving the local state to the full-fledged shared state:

+ import { Store, useStore } from "@t8/react-store";
+
+ let counterStore = new Store(0);

  let Counter = () => {
-   let [counter, setCounter] = useState(0);
+   let [counter, setCounter] = useStore(counterStore);

    let handleClick = () => {
      setCounter(value => value + 1);
    };

    return <button onClick={handleClick}>+ {counter}</button>;
  };

  let ResetButton = () => {
-   let [, setCounter] = useState(0);
+   let [, setCounter] = useStore(counterStore, false);

    let handleClick = () => {
      setCounter(0);
    };

    return <button onClick={handleClick}>×</button>;
  };

  let App = () => <><Counter/>{" "}<ResetButton/></>;

Live counter demo
Tic-tac-toe

🔹 The shared state setup shown above is very similar to useState() allowing for quick migration from local state to shared state or the other way around.

🔹 The false parameter in useStore(store, false) (as in <ResetButton> above) tells the hook not to subscribe the component to tracking the store state updates. The common use case is when a component makes use of the store state setter without using the store state value.

Single store or multiple stores

An application can have as many stores as needed.

🔹 Splitting data into multiple stores is one of the strategies to maintain more targeted subscriptions to data changes in components. The other strategy is filtering store updates at the component level, which is discussed below.

Filtering store updates

When only the store state setter is required, without the store state value, we can opt out from subscription to store state changes by passing false as the parameter of useStore():

let [, setState] = useState(store, false);

Apart from a boolean, useStore(store, shouldUpdate) accepts a function of (nextState, prevState) => boolean as the second parameter to filter store updates to respond to:

let ItemCard = ({ id }) => {
  // Definition of changes in the item
  let hasRelevantUpdates = useCallback((nextItems, prevItems) => {
    // Assuming that items have a `revision` property
    return nextItems[id].revision !== prevItems[id].revision;
  }, [id]);

  let [items, setItems] = useStore(itemStore, hasRelevantUpdates);

  return (
    // Content
  );
};

Providing shared state

Shared state can be provided to the app by means of a regular React Context provider:

import { createContext } from "react";

export let AppContext = createContext(new Store(0));
let App = () => (
  <AppContext.Provider value={new Store(42)}>
    <PlusButton/>{" "}<Display/>
  </AppContext.Provider>
);
let Counter = () => {
  let [counter, setCounter] = useStore(useContext(AppContext));

  // Rendering
};

Live counter demo with Context

🔹 In a multi-store setup, stores can be located in a single Context or split across multiple Contexts, just like any application data.

// Multiple stores in a single Context
let AppContext = createContext({
  users: new Store(/* ... */),
  items: new Store(/* ... */),
});
let ItemCard = ({ id }) => {
  let [items, setItems] = useStore(useContext(AppContext).items);

  // Rendering
};

🔹 Note that updating the store state doesn't change the store reference sitting in the React Context and therefore doesn't cause updates of the entire Context. Only the components subscribed to updates in the particular store by means of useStore(store) will be notified to re-render.

Store data

A store can contain data of any type.

Live demos:
Primitive value state
Object value state

With Immer

Immer can be used with useStore() just the same way as with useState() to facilitate deeply nested data changes.

Live demo with Immer

Shared loading state

The ready-to-use hook from the T8 React Pending package helps manage shared async action state without disturbing the app's state management and actions' code.

Remount-persistent state

A standalone store initialized outside a component can be used by the component as remount-persistent state, whether used by other components or not.