redux-ops
v1.2.0
Published
A Redux reducer/middleware for managing async and operational states.
Downloads
23
Maintainers
Readme
redux-ops
A Redux reducer/middleware for managing asynchronous and operational states.
Getting Started
You can either install the module via npm
or yarn
:
npm install redux-ops --save
yarn add redux-ops
Motivation
Maintaining asynchronous and operational states is an integral part of almost every modern (web) app, but an often discussed topic when it comes to their implementation and the Redux state structure to store them accordingly.
redux-ops
is trying to take this concern away by providing a reducer, an optional middleware with action creator Blueprints, actions, selectors and utilities to
- maintain aforementioned states in a more consistent way (e.g. requests, transaction-like processes),
- communicate these async/operational state transitions,
- prevent cluttering of state slices with individual sub-states
- and to have a centralized place to store them.
Examples
Introduction: Operations
At its core, redux-ops
consists of a reducer with a set of actions for the creation, update and deletion of Operations.
An Operation represents any async or operational task in the form of the following object that gets updated and persisted within the opsReducer
.
{
id: '74168d',
status: 'success',
data: [{ "id": 1, "name": "Jurassic World" }],
}
In the following example, we are going to fetch some movie data from a server and use these core actions to perform the state transitions.
import { createStore, combineReducers } from 'redux';
import opsReducer, { actions, selectors } from 'redux-ops';
// Create store and set up the reducer
const store = createStore(combineReducers({ ops: opsReducer }));
// Create an Operation in its default state
const opId = '74168d';
dispatch(actions.startOperation(opId));
// State => { ops: { id: '74168d', status: 'started' } }
// Fetch movies and update the previously created Operation
fetch('https://example.com/movies.json')
.then(response => response.json())
.then(movies => dispatch(actions.updateOperation(opId, OpStatus.Success, movies)))
.catch(error => dispatch(actions.updateOperation(opId, OpStatus.Error, error.message)));
// State => { ops: { id: '74168d', status: 'success', data: {...} } }
A set of selectors and utility functions allows to, for example, retrieve the current state of an Operation, or clean it up when it's no longer needed.
// Get the Operation state by using one of the provided selectors
console.log(selectors.getOpById(store.getState(), opId));
// Op => { id: '74168d', status: 'success', data: {...} }
// Delete the Operation
dispatch(actions.deleteOperation(opId));
// State => { ops: {} }
Blueprints & Middleware
redux-ops
also comes with an optional middleware that enables the usage of so-called action Blueprints to reduce boilerplate by either using already defined action creators like the ones in the example below or by solely relying on the built-in Operations.
import { createStore, combineReducers, applyMiddleware } from 'redux';
import opsReducer, { opsMiddleware } from 'redux-ops';
// Set up reducer and apply the middleware
const store = createStore(
combineReducers({ ops: opsReducer },
applyMiddleware(opsMiddleware)
);
// We can either create/use existing actions (recommended), or let the Blueprints handle it for us.
const fetchMovies = () => ({ type: movieFetcher.START });
const didFetchMovies = movies => ({ type: movieFetcher.SUCCESS, payload: { movies } });
The createBlueprint
function wraps our action creators into actions that will be processed by the middleware.
Since we already have two designated action creators to initiate (fetchMovies
) and complete (didFetchMovies
) the Operation, we can leverage them, or let the auto-generated action creators handle non-defined cases such as the error
one, which we decided to not define for now.
import { createBlueprint } from 'redux-ops';
// Create a new Blueprint for managing state
const movieFetcher = createBlueprint('FETCH_MOVIES', {
start: fetchMovies,
success: didFetchMovies,
});
When creating a new Blueprint, an id
/action type needs to be passed in. This identifier can be a string
or number
and is, amongst other use cases, also utilized for other concepts such as Operation broadcasting and unique Operations.
To kick-off the Operation, we need to dispatch the movieFetcher.start()
action.
dispatch(movieFetcher.start());
The middleware will process the Blueprint-Action, start a new Operation with the id FETCH_MOVIES
(which can be chosen arbitrarily and is not directly related to existing types) and additionally dispatch the original fetchMovies()
action.
The request itself can then be sent in whatever way you prefer.
// Fetch movies and update the previously started Operation
fetch('https://example.com/movies.json')
.then(response => response.json())
.then(movies => dispatch(movieFetcher.success(movies)))
.catch(error => dispatch(movieFetcher.error(error.message));
// Get the Operation state by using one of the provided selectors
console.log(selectors.getOpById(store.getState(), movieFetcher.id));
// Delete the Operation if needed
dispatch(movieFetcher.delete());
Documentation
License
MIT