Skip to content

t8js/react-store

Repository files navigation

npm Lightweight TypeScript ✓ CSR ✓ SSR ✓

@t8/react-store

Straightforward and minimalist 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 { createContext, useContext } from "react";
+ import { Store, useStore } from "@t8/react-store";
+
+ let AppContext = createContext(new Store(0));

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

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

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

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

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

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

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

Live counter demo
Tic-tac-toe

🔹 The shared state setup with @t8/react-store 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, whether on a single React Context or multiple Contexts.

let AppContext = createContext({
  users: new Store(/* ... */),
  items: new Store(/* ... */),
});

🔹 Splitting data into multiple stores helps maintain more targeted subscriptions to data changes in components.

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) can take 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(
    useContext(AppContext).items,
    hasRelevantUpdates,
  );

  return (
    // Content
  );
};

Providing shared state

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

  let App = () => (
-   <AppContext.Provider value={42}>
+   <AppContext.Provider value={new Store(42)}>
      <PlusButton/>{" "}<Display/>
    </AppContext.Provider>
  );

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.

Standalone store

A store initialized outside a component can be used as the component's remount-persistent state.