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

@axtk/react-store

v3.0.3

Published

Compact shared-state management in React

Downloads

137

Readme

npm GitHub browser SSR TypeScript

@axtk/react-store

Compact shared-state management in React

Taking inspiration from the easiness of using local state with the useState hook.

Usage example

This package provides the Store class and the useStoreListener hook. The Store class is a container for shared data that allows for subscription to its updates, and the useStoreListener hook makes these subscriptions from within React components. The sharing of the stores across components is performed by means of React's Context in a pretty straightforward manner, as shown in the example below.

import {createContext, useContext} from 'react';
import ReactDOM from 'react-dom';
import {Store, useStoreListener} from '@axtk/react-store';

// Creating a React Context that will be furnished with a
// store in a `StoreContext.Provider` component below.
const StoreContext = createContext();

// Making up a helper hook that picks the store from the Context
// and makes a subscription to the store updates.
const useStore = () => {
    const store = useContext(StoreContext);
    useStoreListener(store);
    return store;
};

// Both `PlusButton` and `Display` below subscribe to the same
// store and thus share the value of `n` contained in the store.

const PlusButton = () => {
    const store = useStore();
    return (
        <button onClick={
            () => store.set('n', store.get('n') + 1)
        }>
            +
        </button>
    );
};

const Display = () => {
    const store = useStore();
    return <span>{store.get('n')}</span>;
};

const App = () => <div><PlusButton/> <Display/></div>;

ReactDOM.render(
    // Initializing the context with a store.
    // The constructor of the Store class accepts an (optional)
    // initial state.
    <StoreContext.Provider value={new Store({n: 42})}>
        <App/>
    </StoreContext.Provider>,
    document.querySelector('#app')
);

This example covers much of what is needed to deal with a store in a React app, although there are in fact another couple of methods in the Store API.

From the context's perspective, the store as a data container never changes after it has been initialized concealing its updates under the hood. All interactions with the shared context data are left to the store itself, without the need to come up with additional utility functions to mutate the data in order to trigger a component update.

Multi-store setup

The shape of a React's context can be virtually anything. It means a single context can accommodate several stores. The task is still to pick the store from the context and to subscribe to its updates by means of the useStoreListener hook.

Having multiple stores can help to convey the semantic separation of data in the application and to avoid component subscriptions to updates of irrelevant chunks of data.

import {createContext, useContext} from 'react';
import ReactDOM from 'react-dom';
import {Store, useStoreListener} from '@axtk/react-store';

const StoreContext = createContext({});

// A helper hook for quicker access to the specific store
// from within the components
const useTaskStore = () => {
    const {taskStore} = useContext(StoreContext);
    useStoreListener(taskStore);
    return taskStore;
};

const Task = ({id}) => {
    const taskStore = useTaskStore();
    const task = taskStore.get(id);
    return task && <div class="task">{task.name}: {task.status}</div>;
};

const App = () => {
    // Fetching, pushing to the store and rendering multiple tasks
};

ReactDOM.render(
    <StoreContext.Provider value={{
        taskStore: new Store(),
        userStore: new Store()
    }}>
        <App/>
    </StoreContext.Provider>,
    document.querySelector('#app')
);

Server-side rendering (SSR)

On the server, the stores can be pre-filled and passed to a React Context in essentially the same way as in the client-side code.

// On an Express server
app.get('/', prefetchAppData, (req, res) => {
    const html = ReactDOMServer.renderToString(
        <StoreContext.Provider value={new Store(req.prefetchedAppData)}>
            <App/>
        </StoreContext.Provider>
    );

    const serializedAppData = JSON.stringify(req.prefetchedAppData)
        .replace(/</g, '\\x3c');

    res.send(`
        <!doctype html>
        <html>
            <head><title>App</title></head>
            <body>
                <div id="app">${html}</div>
                <script>
                    window._prefetchedAppData = ${serializedAppData};
                </script>
                <script src="/index.js"></script>
            </body>
        </html>
    `);
});

When the application is rendered in the browser, the browser store instance can be filled with the serialized data to match the rendered state.

// On the client
ReactDOM.hydrate(
    <StoreContext.Provider value={new Store(window._prefetchedAppData)}>
        <App/>
    </StoreContext.Provider>,
    document.querySelector('#app')
);

Local store

A component-scoped store can act as a local state persistent across remounts and as an unmount-safe storage for async data.

import {useEffect} from 'react';
import {Store, useStoreListener} from '@axtk/react-store';

const itemStore = new Store();

const Item = ({id}) => {
    useStoreListener(itemStore);

    useEffect(() => {
        if (itemStore.get(id)) return;

        itemStore.set(id, {loading: true});

        fetch(`/items/${id}`)
            .then(res => res.json())
            .then(data => itemStore.set(id, {data, loading: false}));
        // If the request completes after the component has unmounted
        // the fetched data will be safely put into `itemStore` to be
        // reused when/if the component remounts.

        // Data fetching error handling was not added to this example
        // only for the sake of focusing on the interaction with the
        // store.
    }, [itemStore]);

    let {data, loading} = itemStore.get(id) || {};

    // Rendering
};

export default Item;

Optional fine-tuning

By default, each store update will request a re-render of the component subscribed to the particular store, which is then optimized by React with its virtual DOM reconciliation algorithm before affecting the real DOM (and this can be sufficient in many cases). The function passed to the useStoreListener hook as the optional second parameter can prevent the component from some re-renders at an even earlier stage if its returned value hasn't changed.

useStoreListener(store, store => store.get([taskId, 'timestamp']));
// In this example, a store update won't request a re-render if the
// timestamp of the specific task hasn't changed.
useStoreListener(store, null);
// With `null` as the second argument, the store updates won't cause
// any component re-renders.

The optional third parameter allows to specify the value equality function used to figure out whether the update trigger value has changed. By default, it is Object.is.

useStoreListener(
    store,
    store => store.get([taskId, 'timestamp']),
    (prev, next) => next - prev < 1000
);
// In this example, a store update won't request a re-render if the
// timestamp of the specific task has increased by less than a
// second compared to the previous timestamp value.

Also