npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

groundstate

v1.0.5

Published

Minimalist shared state management for React apps

Downloads

420

Readme

groundstate

Minimalist shared state management for React apps

  • Easy to set up, similar to useState(), no boilerplate code
  • Painless transition from local state to shared state and vice versa
  • SSR-compatible
  • Lightweight

Installation: npm i groundstate

Usage

Let's take two components containing counters stored in their local states via React's useState(), isolated from each other. Let's see what should be edited to share the counter between these components.

import {createContext, useContext} from 'react';
+ import {Store, useStore} from 'groundstate';

+ let AppContext = createContext(new Store(0));

let Display = () => {
-   let [counter] = useState(0); // somewhat contrived, never updated
+   let [counter] = useStore(useContext(AppContext));

    return <span>{counter}</span>;
};

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

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

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

let App = () => <><PlusButton/>{' '}<Display/></>;

After the edits, whenever the counter is updated by clicking PlusButton, Display gets notified and re-rendered with the new counter value.

Note how little change is required to replace local state with shared state, which is a typical task in the development of an app (and yet not so quickly achieved with many other approaches to shared state management).

The Store class and the useStore() hook together do the trick of the shared state management.

Fine-tuning responsiveness to store updates

You might have noticed the false parameter of useStore() in PlusButton. This is a way to tell the hook not to re-render the component when the store gets updated. Unlike Display, PlusButton doesn't use the counter value, so it doesn't need to track the store updates.

Apart from a boolean value, the second parameter of useStore() can also be a function of (nextState, prevState) returning a boolean, allowing for subtler fine-tuning of responsiveness to store updates.

Store provider

You might also notice there's no Context Provider in the example above: the components make use of the default Context value passed to createContext(). In more complex apps (especially with SSR), an appropriate Context Provider can be added to specify the initial state:

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

Store data

In the example above, an instance of the Store class wraps a primitive value, but there can be data of any type.

Multiple stores

An application can have as many stores as needed, whether on a single Context or multiple Contexts.

Splitting the app data into multiple stores

  • makes the scopes of the stores clearer,
  • helps reduce irrelevant update notifications in the components requiring only a limited portion of the data.
let AppContext = createContext({
    users: new Store(/* ... */),
    articles: new Store(/* ... */),
});

let UserCard = ({userId}) => {
    let [users, setUsers] = useStore(useContext(AppContext).users);

    // ...
};

In this example, the UserCard component uses only the users store from AppContext. It won't be re-rendered if the contents of the articles store gets updated (just as intended).

Note that a store is picked from the Context just like any other value on a Context. The Context may as well contain other non-store items alongside stores if need be. A store (whether from the Context or elsewhere) is passed to the useStore() hook to unpack the current store state and subscribe the component to the store updates.

Other use cases

Persistent local state

Maintaining local state of a component with the React's useState() hook is commonplace and works fine for many cases, but it has its downsides in the popular scenarios:

  • the updated state from useState() is lost whenever the component unmounts, and
  • setting the state in an asynchronous callback after the component gets unmounted causes an error that requires extra handling.

Both of these issues can be addressed by using a store created outside of the component instead of useState(). Such a store doesn't have to be shared with other components (although it's also possible) and it will act as:

  • local state persistent across remounts, and
  • unmount-safe storage for asynchronously fetched data.
+ let itemStore = new Store();

let List = () => {
-   let [items, setItems] = useState();
+   let [items, setItems] = useStore(itemStore);

    useEffect(() => {
        if (items !== undefined)
            return;

        fetch('/items')
            .then(res => res.json())
            .then(items => setItems(items));
    }, [items]);

    // ... rendering
};

In the example above, if the request completes after the component has unmounted the fetched data will be safely put into itemStore and this data will be reused when the component remounts without fetching it again.

Connecting a store to external storage

itemStore from the example above can be further upgraded to make the component state persistent across page reloads without affecting the component's internals.

let initialState;

try {
    initialState = JSON.parse(localStorage.getItem('list'));
}
catch {}

export let itemStore = new Store(initialState);

itemStore.subscribe(nextState => {
    localStorage.setItem('itemStore', JSON.stringify(nextState));
});
import {itemStore} from './itemStore';

let List = () => {
    let [items, setItems] = useStore(itemStore);

    // ...
};

Direct subscription to store updates

For some purposes (like logging or debugging the data flow), it might be helpful to directly subscribe to state updates via the store's subscribe() method:

let App = () => {
    let store = useContext(AppContext);

    useEffect(() => {
        // `subscribe()` returns an unsubscription function which
        // works as a cleanup function in the effect.
        return store.subscribe((nextState, prevState) => {
            console.log({nextState, prevState});
        });
    }, [store]);

    // ...
};

Adding immer

immer is not part of this package but it can be used with useStore() just the same way as with useState() to facilitate deeply nested data changes.