A gray cat sitting on a desk, surrounded by code snippets and a laptop.

Introduction

The ‘react-use-sub’ library is a lightweight and efficient way to manage state in your React applications. It allows you to create a subscription-based store that can be easily integrated into your existing codebase. This library is designed to be simple to use, easy to test, and has no dependencies. It also supports TypeScript and is very small in size, making it ideal for performance-critical applications.

Installation

You can install ‘react-use-sub’ using npm or yarn:

Using npm

npm install react-use-sub

Using yarn

yarn add react-use-sub

Basic Usage

To use ‘react-use-sub’, you need to create a store and then subscribe to it in your components. Here is a simple example:

// store.js
import { createStore } from 'react-use-sub';

const initialState = { foo: 'bar', num: 2 };
export const [useSub, Store] = createStore(initialState);

// component.js
import { useSub } from '/path/to/store.js';

export const App = () => {
    const { fooLength, num } = useSub(({ foo, num }) => ({ fooLength: foo.length, num }));
    const square = useSub(({ num }) => num**2);

    return <div>Magic number is: {fooLength * num * square}</div>;
}

In this example, we create a store with an initial state and then subscribe to it in the App component. We use the useSub hook to subscribe to the store and get the values we need.

Advanced Usage

Conditional Updates

When setting the store, you can specify conditions to update only if certain values change. For example:

Store.set(({ articles }) => (articles.length ? { stock: articles.length } : undefined));

Shallow Equality Optimization

The library performs shallow equality checks to determine if a rerender is necessary. This means that if the value of foo does not change, the component will not rerender.

Multiple Subscriptions in a Single Component

You can subscribe to multiple values in a single component:

export const GreatArticle = () => {
    const { id, author, title } = useSub(({ article }) => article);
    const reviews = useSub(({ reviews }) => reviews);
    const [trailer, recommendation] = useSub(({ trailers, recommendations }) => [trailer[id], recommendations[id]]);

    return <>...</>;
}

Multiple Store Updates

You can perform multiple store updates in a single synchronous task without causing unnecessary rerenders:

Store.set({ foo: 'bar' });
Store.set({ num: 2 });
Store.set({ lastVisit: new Date() });

Multiple Stores

You can create multiple stores and subscribe to them separately:

import { createStore } from 'react-use-sub';

export const [useArticleSub, ArticleStore] = createStore(initialArticleState);

export const [useCustomerSub, CustomerStore] = createStore(initialCustomerState);

export const [useEventSub, EventStore] = createStore(initialEventState);

Persisting Data on the Client

You can persist data on the client using local storage or other methods:

const usePersistArticles = () => {
    const articles = useSub(({ articles }) => articles);

    useEffect(() => {
        localStorage.setItem('articles', JSON.stringify(articles));
    }, [articles]);
};

const localStorageArticles = localStorage.getItem('articles');
const initialState = {
    articles: localStorageArticles ? JSON.parse(localStorageArticles) : {},
}

const [useSub, Store] = createStore(initialState);

Middlewares

You can write custom middlewares to handle special state updates:

import { createStore, StoreSet } from 'react-use-sub';

type State = { conversionStep: number };
const initialState: State = { conversionStep: 1 };

const [useSub, _store] = createStore<State>(initialState);

const set: StoreSet<State> = (update) => {
    const prevState = _store.get();
    _store.set(update);
    const state = _store.get();
    if (prevState.conversionStep !== state.conversionStep) {
        trackConversionStep(state.conversionStep)
    }
}

const Store = { ..._store, set, reset: () => _store.set(initialState) };

export { useSub, Store };

Testing

You can test your store using the ‘test-util’ package:

import 'react-use-sub/test-util';

describe('<MyExample />', () => {
    it('renders the stock', () => {
        // initialization
        Store.set({ article: { stock: 1337 } as any });

        // render with stock 1337
        const { container } = render(<MyExample />);
        expect(container.textContent).toBe('Article stock is: 1337');

        // update the stock
        Store.set({ article: { stock: 444 } as any });
        expect(container.textContent).toBe('Article stock is: 444');
    });
});

SSR

For SSR, you can create a store instance that is provided by a React context:

import React, { useMemo } from 'react';
import { createStore, StoreType, UseSubType } from 'react-use-sub';

const initialState = { foo: 'bar', num: 2 };
type State = typeof initialState;
const Context = React.createContext<{ useSub: UseSubType<State>; store: StoreType<State> }>({} as any);

export const useStore = (): StoreType<State> => React.useContext(Context).store;
export const useSub: UseSubType<State> = (...args) => React.useContext(Context).useSub(...args);

export const StoreProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
    const value = useMemo(() => {
        const [useSub, store] = createStore(initialState);
        return { useSub, store };
    }, []);

    return <Context.Provider value={value}>{children}</Context.Provider>;
};

Conclusion

The ‘react-use-sub’ library is a powerful tool for managing state in your React applications. It is lightweight, easy to use, and supports TypeScript. With its subscription-based approach, you can create complex applications with ease. Whether you are building a simple to-do list or a complex e-commerce application, ‘react-use-sub’ can help you manage your state efficiently.

Comments