Skip to content

A state management solution built on top of Redux, inspired by Recoil

License

Notifications You must be signed in to change notification settings

atomic-redux/atomic-redux

Repository files navigation

Atomic Redux State

An application state management solution built on top of Redux, inspired by Recoil.

Why?

atomic-redux-state uses a simple API for managing global application state, aiming to reduce the boilerplate code prevalent in regular Redux. This API is clearer to read, and makes managing derived state easier.

Internally, it automatically builds a directed acyclic graph of dependencies, to prevent unneccesary re-execution of selectors, without requiring manual memoisation like Redux selectors do.

Recoil, the library that inspired this package, has an excellent developer-friendly API for managing application state, especially derived state or dynamic data. However, it can only be used within a React context, which may not be suitable for all applications. For example, a system that integrates some React components into an existing website cannot (easily) get the data stored in Recoil atoms.

This library also allows interaction with atoms within Redux middleware, for example in Redux Sagas.

Installation

First, set up a Redux store and provider, as described in the Redux Toolkit getting started guide.

Install the core and React libraries using yarn add atomic-redux-state atomic-redux-state-react. If not using with React, atomic-redux-state-react can be omitted.

You must then add the atom middleware and reducer to your Redux store.

import { atomsReducer, getAtomMiddleware } from 'atomic-redux-state';
import { combineReducers, configureStore } from '@reduxjs/toolkit';

const store = configureStore({
    reducer: combineReducers({
        atoms: atomsReducer
        // Other reducers
    }),
    middleware: [
        // Other middleware
        getAtomMiddleware()
    ]
});

If using React, change your Redux provider to an Atomic Redux provider:

import { AtomicReduxProvider } from 'atomic-redux-state-react';

root.render(
    <AtomicReduxProvider store={store}>
      <App />
    </AtomicReduxProvider>
);

Usage

Most principles from Recoil apply to atomic-redux-state.

Atom

An atom is the simplest type of state in atomic-redux-state. An atom holds a unit of state, and can be shared by multiple components. Any updates to an atom are syncronised across all components that use it.

An atom can hold simple primitive types, or any serialisable object (e.g. no functions).

A default initial value must be provided for an atom.

The example below demonstrates creating a simple atom used by two components, with the value and changes to it being syncronised between them.

import { atom } from 'atomic-redux-state';

const myAtom = atom({
    key: 'my-atom',
    default: 0
});
import { useAtomicState } from 'atomic-redux-state-react';
import { myAtom } from './atoms';

export const MyComponent = () => {
    const [value, setValue] = useAtomicState(myAtom);

    return
        <div>
            <button onClick={() => setValue(value => value - 1)}>Decrement</button>
            <span>{value}</span>
            <button onClick={() => setValue(value => value + 1)}>Increment</button>
        </div>
};
import { useAtomicState } from 'atomic-redux-state-react';
import { myAtom } from './atoms';

export const MyOtherComponent = () => {
    const [value, setValue] = useAtomicState(myAtom);

    return
        <div>
            <button onClick={() => setValue(value => value - 10)}>Add</button>
            <span>{value}</span>
            <button onClick={() => setValue(value => value + 10)}>Subtract</button>
        </div>
};

Derived atom

Atom state can also be derived from other atoms. The derived state will change whenever the state it depends on is updated. This creates a data-flow graph for your application state.

Note that in Recoil, this concept is called a selector. However, to avoid conflicting with the Redux concept of selectors, atomic-redux-state refers to these as "derived atoms", created using the derivedAtom function.

The below example shows the creation of a derived atom that will always have a value that is double the value of myAtom.

import { atom, derivedAtom } from 'atomic-redux-state';

const myAtom = atom({
    key: 'my-atom',
    default: 0
});

const multipliedValueAtom = derivedAtom({
    key: 'multiplied-value',
    get: ({ get }) => {
        const originalValue = get(myAtom);
        return originalValue * 2;
    }
});

By default derived atoms are readonly, so should be read using useAtomicValue()

const value = useAtomicValue(multipliedValueAtom);

Derived atoms can also specify a set method, which makes the derived atom writable and allows it to update the atoms it depends upon.

const multipliedValueAtom = derivedAtom<number>({
    key: 'multiplied-value',
    get: ({ get }) => {
        return get(myAtom) * 2;
    },
    set: ({ set }, value) => {
        set(myAtom, value / 2);
    }
});

Once a derived atom specifies a set method, it becomes writable and can now be consumed like a regular atom.

const [value, setValue] = useAtomicState(multipliedValueAtom);

Async get

The get method on a derived atom can also be async

const userDataAtom = derivedAtom<UserData>({
    key: 'user-data',
    get: async () => {
        return await Api.getUserData();
    }
});

Async atoms and atoms that depend on them will return LoadingAtom until a value is returned by the promise, allowing the component consuming it to display a placeholder until there is data.

export const UserDataDisplay = () => {
    const userData = useAtomicValue(userDataAtom); // typeof userData = UserData | LoadingAtom

    return
        <div>
            {userData instanceof LoadingAtom
                ? <span>Loading...</span>
                : <span>Hello {userData.firstName} {userData.lastName}!</span>
            }
        </div>
}

Async atom updates

If an atom that an async atom depends on updates, and the async atom already has a value, it maintains that value until the promise resolves. Once an async atom promise resolves, it's value does not go back to LoadingAtom when it re-updates.

Instead, check if an atom is updating from its current value using useIsAtomUpdating(atom)

const selectedUserIdAtom = atom({
    key: 'selected-user-id',
    default: 1
})

const userDataAtom = derivedAtom<UserData>({
    key: 'user-data',
    get: async ({ get }) => {
        const selectedUserId = get(selectedUserIdAtom);
        return await Api.getUserData(selectedUserId);
    }
});
export const UserDataDisplay = () => {
    const userData = useAtomicValue(userDataAtom); // typeof userData = UserData | LoadingAtom
    const isUpdating = useIsAtomUpdating(userDataAtom);

    return
        <div>
            {userData instanceof LoadingAtom || isUpdating
                ? <span>Loading...</span>
                : <span>Hello {userData.firstName} {userData.lastName}!</span>
            }
        </div>
}

Usage outside of React context

Initialising an atom - initialiseAtom

Before an atom can be used outside of the React context, it must be initialised by dispatching the initialiseAtom action to the store:

import { initialiseAtom } from `atomic-redux-state`;

store.dispatch(initialiseAtom(myAtom));

Getting atom values - selectAtom

The selectAtom(store, atom) method gets an atom value from the Redux store. If the atom has not been initialised, this could return undefined.

import { selectAtom } from 'atomic-redux-state';

const atomValue = selectAtom(store, myAtom);

Initialise and get - initialiseAtomFromStore

The initialiseAtomFromStore(store, atom) method combines the initialise and select methods, allowing the retrieval of an atom value without dispatching the initialiseAtom action first. Since this initialises the atom first, the atom value will not be undefined.

import { initialiseAtomValueFromStore } from 'atomic-redux-state';

const atomValue = initialiseAtomFromStore(store, myAtom);

Setting atom values outside React context - setAtom

The setAtom(atom, value) action creator allows you to set atom values outside of the React context, or in a Redux middleware such as Sagas.

Dispatch the action created by setAtom to update the atom value.

import { setAtom } from 'atomic-redux-state';

store.dispatch(setAtom(myAtom, 10));

// selectAtom(store, myAtom) now returns 10

Accessing from Redux Sagas

Atoms can be accessed using the selectAtom selector and the setAtom action.

import { setAtom, selectAtom } from 'atomic-redux-state';

export function* mySaga() {
    const value = yield select(selectAtom, myAtom);
    yield put(setAtom, myOtherAtom, 10);
}

About

A state management solution built on top of Redux, inspired by Recoil

Resources

License

Stars

Watchers

Forks

Packages

No packages published