11 Redux
Redux is a framework for managing the state of JavaScript applications. In React state is managed by components, and passed down the component tree via props. This works well for small applications, but becomes difficult to manage for larger applications. Redux provides a central store for the state of the application.
Principles
Redux is based on three principles:
- Single source of truth: The state of the whole application is stored in an object tree within a single store.
- State is read-only: The only way to change the state is to emit an action, an object describing what happened.
- Changes are made with pure functions: To specify how the state tree is transformed by actions, you write pure reducers.
Data Flow
The data in a Redux application follows a unidirectional flow. Views emit events which are converted to actions. Actions are dispatched to invoke reducers, which modify the state. Views are updated when the state changes.
graph LR
A[Action]
D[Dispatcher]
R[Reducer]
S[State]
V[View]
A -->|action| D
S -->|state| V
V -->|event| A
subgraph Store
D -->|action| R
R --> S
S -->|state| R
end
Example
The following example is a simple counter application that uses Redux to manage the state.
const redux = require("redux");
// initial state
const initialState = { count: 0 };
// reducer
const reducer = (state = initialState, action) => {
switch (action.type) {
case "INCREMENT": return { count: state.count + 1 };
case "DECREMENT": return { count: state.count - 1 };
default: return state;
}
};
// create store from reducer
const store = redux.createStore(reducer);
// simulate view (subscribe to store)
store.subscribe(() => console.log(store.getState()));
// dispatch actions
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "DECREMENT" });
Middleware
Middleware provides a third-party extension point between dispatching an action and the moment it reaches the reducer. It can be used for logging, crash reporting, performing asynchronous tasks, etc.
Example
The following example shows a simple logger middleware.
const redux = require("redux");
// initial state
const initialState = { count: 0 };
// reducer
const reducer = (state = initialState, action) => {
switch (action.type) {
case "INCREMENT": return { count: state.count + 1 };
case "DECREMENT": return { count: state.count - 1 };
default: return state;
}
};
// logger middleware
const logger = store => next => action => {
console.log("dispatching", action);
const result = next(action);
console.log("next state", store.getState());
return result;
};
// create store from reducer
const store = redux.createStore(reducer, redux.applyMiddleware(logger));
// simulate view (subscribe to store)
store.subscribe(() => console.log(store.getState()));
// dispatch actions
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "DECREMENT" });
Async Actions
Actions are normally plain objects.
Middleware can be used to dispatch functions, which are called with the dispatch and getState functions as arguments.
This is useful for performing asynchronous tasks.
In general, async actions usually dispatch three different kinds of actions:
- An action informing the reducers that the request began.
- An action informing the reducers that the request finished successfully.
- An action informing the reducers that the request failed.
Example
The following example simulates an asynchronous action.
const redux = require("redux");
// initial state
const initialState = { count: 0, waiting: false };
// reducer
const reducer = (state = initialState, action) => {
switch (action.type) {
case "INCREMENT": return { count: state.count + 1 };
case "DECREMENT": return { count: state.count - 1 };
case "WAITING": return { ...state, waiting: true };
case "RECEIVED": return { ...state, waiting: false };
default: return state;
}
};
// create store from reducer
const store = redux.createStore(reducer, redux.applyMiddleware(thunk));
// create async action
const incrementAsync = () => {
return dispatch => {
dispatch({ type: "WAITING" });
setTimeout(() => {
dispatch({ type: "INCREMENT" })
dispatch({ type: "RECEIVED" });
}, 1000);
};
};
// simulate view (subscribe to store)
store.subscribe(() => console.log(store.getState()));
// dispatch action (first waiting, after 1 second increment)
store.dispatch(incrementAsync());
Reducers
Reducers reduce a sequence of actions into a state. They are, in a sense, like accumulator functions.
For bigger applications, it is recommended to split the root reducer into smaller reducers that manage specific parts of the state tree.
The combineReducers() helper function can be used to combine several reducers into one.
Example
The following example shows how to use combineReducers().
const redux = require("redux");
// initial state
const initialState = { count: 0, waiting: false };
// counter reducer
const counterReducer = (state = initialState.count, action) => {
switch (action.type) {
case "INCREMENT": return state + 1;
case "DECREMENT": return state - 1;
default: return state;
}
};
// waiting reducer
const waitingReducer = (state = initialState.waiting, action) => {
switch (action.type) {
case "WAITING": return true;
case "RECEIVED": return false;
default: return state;
}
};
// root reducer
const rootReducer = redux.combineReducers({
count: counterReducer,
waiting: waitingReducer
});
// create store from reducer
const store = redux.createStore(rootReducer);
// ...
React
React bindings for Redux are available as the react-redux package.
The hooks can be found on the React Redux Hooks documentation page.
Using Context
The react-redux package uses the React Context API to provide the store to the components.
The following example shows how to use the Context API directly.
Zustand
https://www.npmjs.com/package/zustand is a small, fast and scalable state management library for React.