Redux is a javascript library used in a lot of projects, which helps to manage a global state. It exists some binding libraries to use Redux with React: react-redux , Angular: ng-redux , Ember: ember-redux , …
In this article I will not explain the best practices on how to use Redux. If you want more explanation on how to us it, you can see the documentation which is awesome: https://redux.js.org/
Basically, you have a single state for your whole application and this state must stay immutable.
A reducer is a pure function, it is the only one which can change the state (sometimes called also store). The first parameter of this method is the current state and the second one the action to handle:
The action is a simple object which is often represented with:
- type: the type of the action to process
- payload: the data useful to process the action
const initialState = { userName: undefined };
export default function userReducer(
state = initialState,
action,
) {
switch (action.type) {
case 'SET_USERNAME': {
// The state must stay immutable
return { ...state, userName: action.payload };
}
default:
return state;
}
}
To create a store, you have to use the method createStore
and give it the reducer(s) in first parameter:
import { createStore } from 'redux';
import userReducer from './userReducer';
const store = createStore(userReducer);
With this store created, you can get two methods:
getState
to get the current statedispatch
to dispatch actions wich will be passed to reducers
store.dispatch({
type: 'SET_USERNAME',
payload: 'Bob the Sponge',
});
const state = store.getState();
console.log(state.userName); // Print "Bob the Sponge"
Well, Rainbow, you told us that you will explain what is under the hood and finally you explain how to use it.
Sorry guys, I needed to put some context before going deep into Redux ;)
In all the links you will see below I browse the code of the commit with the sha 176e66adc9a90df
.
Actually createStore
is a closure which has an
object state
et return the methods getState
and dispatch
:
function createStore(reducer) {
let state;
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
return action;
};
// Populates the state with the initial values of reducers
dispatch({ type: '@@redux/INIT' });
return { getState, dispatch };
}
For the moment, we saw a simple case with a single reducer. But in applications you usually more than one otherwise redux
is maybe a little bit
overkill for your use case.
Redux can structure the store in a clean way, by dividing our store.
Let’s go use the function combineReducers
.
For example, with the previous reducer userReducer
, and the new one settingsReducer
:
const initialState = { maxSessionDuration: undefined };
export default function settingsReducer(
state = initialState,
action,
) {
switch (action.type) {
case 'SET_': {
return {
...state,
maxSessionDuration: action.payload,
};
}
default:
return state;
}
}
The combination of reducers will be:
import { combineReducers } from 'redux';
import userReducer from './userReducer';
import settingsReducer from './settingsReducer';
export default combineReducers({
user: userReducer,
settings: settingsReducer,
});
We will get the state:
{
user: {
userName: undefined,
},
settings: {
maxSessionDuration: undefined,
},
};
Knowing that the code of createStore
doesn’t change,
how does combineReducers
work?
function combineReducers(reducersByNames) {
return (state, action) => {
let hasChanged = false;
const nextState = {};
Object.entries(reducersByNames).forEach(
([reducerName, reducer]) => {
// A reducer cannot access states of other ones
const previousReducerState = state[reducerName];
// Calculate the next state for this reducer
const nextReducerState = reducer(
previousReducerState,
action,
);
nextState[reducerName] = nextReducerState;
hasChanged =
hasChanged ||
nextReducerState !== previousReducerState;
},
);
// If there is no changes, we return the previous state
// (we keep the reference of the state for performance's reasons)
return hasChanged ? nextState : state;
};
}
A listener is a callback we can subscribe
to potential changes of the Redux state. This listener is directly executed after an event is dispatched. Previously I talked about potential changes
because, after an action has been dispatched, there is not necessarily changes, for example if none of the reducers know how to handle the event.
Once subscribed, we get a callback to be able to unsubscribe it.
For example if you don’t want, or can’t use the plugin Reduc DevTools
. It can be useful to be able to see the Redux state at any time. In this you
can use a listener:
import { createStore } from 'redux';
import userReducer from './userReducer';
const store = createStore(userReducer);
store.subscribe(
() => (window.reduxState = store.getState()),
);
And now you can see, at any time, the state by typing in your favorite browser console: reduxState
.
Our createStore
becomes:
function createStore(reducer) {
let state;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach((listener) => listener());
return action;
};
const subscribe = (listener) => {
listeners = [...listeners, listener];
return () => {
listeners = listeners.filter((l) => l !== listener);
};
};
dispatch({ type: '@@redux/INIT' });
return { getState, dispatch, subscribe };
}
If you use RxJS
, the store is an Observable
, so that you can add an Observer
to be notified of state’s
changes.
import { from } from 'rxjs';
import { createStore } from 'redux';
import userReducer from './userReducer';
const store = createStore(userReducer);
const myObserver = {
next: (newState) =>
console.log('Le nouveau redux state est: ', newState),
};
from(store).subscribe(myObserver);
// Let's change the username
store.dispatch({
type: 'SET_USERNAME',
payload: "Bob l'éponge",
});
To be an Observable
, the store implements the symbol Symbol.observable
.
Its implementation is really simple because it reuses the implementation of listeners
in a method
observable
:
function createStore(reducer) {
let state;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach((listener) => listener());
return action;
};
const subscribe = (listener) => {
listeners = [...listeners, listener];
return () => {
listeners = listeners.filter((l) => l !== listener);
};
};
const observable = () => ({
subscribe: (observer) => {
// The method `observeState` only notifies
// the Observer of the current value of the state
function observeState() {
observer.next(getState());
}
// As soon as the Observer subscribes
// we send the current value of the state
observeState();
// We refirster the `observeState` function
// as a listener to be notified of next changes of the state
const unsubscribe = listenerSubscribe(observeState);
return {
unsubscribe,
};
},
});
dispatch({ type: '@@redux/INIT' });
return {
getState,
dispatch,
subscribe,
[Symbol.observable]: observable,
};
}
When you use code splitting, it can happened you do not have all reducers when you create the store. To be able to register new reducers after store
creation, redux give us access to the method replaceReducer
which allow for replacement of reducers with new ones:
function createStore(reducer) {
let state;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach((listener) => listener());
return action;
};
const subscribe = (listener) => {
listeners = [...listeners, listener];
return () => {
listeners = listeners.filter((l) => l !== listener);
};
};
const observable = () => {
const listenerSubscribe = subscribe;
return {
subscribe: (observer) => {
function observeState() {
observer.next(getState());
}
observeState();
const unsubscribe = listenerSubscribe(observeState);
return {
unsubscribe,
};
},
};
};
const replaceReducer = (newReducer) => {
reducer = newReducer;
// Like the action `@@redux/INIT`, this one
// populated the state with initial values of new reducers
dispatch({ type: '@@redux/REPLACE' });
};
dispatch({ type: '@@redux/INIT' });
return {
getState,
dispatch,
subscribe,
[Symbol.observable]: observable,
replaceReducer,
};
}
Let’s use this new method replaceReducer
to register a new reducer. At the store creation we only register the reducer userReducer
, then we
register the reducer counterReducer
:
export default function counterReducer(
state = { value: 0 },
action,
) {
switch (action.type) {
case 'INCREMENT': {
return { ...state, value: state.value + 1 };
}
default:
return state;
}
}
The replacement of reducers will be:
import { createStore, combineReducers } from 'redux';
import userReducer from 'userReducer';
import counterReducer from 'counterReducer';
const store = createStore(
combineReducers({ user: userReducer }),
);
console.log(store.getState());
// Prints { user: { userName: undefined } }
store.replaceReducer(
combineReducers({
user: userReducer,
counter: counterReducer,
}),
);
console.log(store.getState());
// Prints { user: { userName: undefined }, counter: { value: 0 } }
A middleware is a tool that we can put between two applications. In the Redux case, the middleware will be placed between the dispatch call and the reducer. I talk about a middleware (singular form), but in reality you can put as much as middleware you want.
An example of middleware is a middleware to log dispatched actions and then the new state.
I’m gonna directly give you the form of a middleware without explanation because I will never do better than the official documentation .
const myMiddleware = (store) => (next) => (action) => {
// With the store you can get the state with `getState`
// or the original `dispatch` `next`represents the next dispatch
return next(action);
};
Example: middleware of the loggerMiddleware
const loggerMiddleware = (store) => (next) => (action) => {
console.log(`I'm gonna dispatch the action: ${action}`);
const value = next(action);
console.log(`New state: ${value}`);
return value;
};
Until now we dispatched actions synchronously. But in an application it can happened we would like to dispatch actions asynchronously. For example, after having resolved an AJAX call with axios (or another library, or directly with XMLHttpRequest if you are juste the boss :p).
The implementation is really simple, if the action dispatched is a function, it will execute it with getState
and dispatch
as parameters. And if
it’s not a function, it passes the action to the next middleware or reducer (if there is no next middleware).
const reduxThunkMiddleware =
({ getState, dispatch }) =>
(next) =>
(action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
The thunk action creator will be:
function thunkActionCreator() {
return ({ dispatch }) => {
return axios.get('/my-rest-api').then(({ data }) => {
dispatch({
type: 'SET_REST_DATA',
payload: data,
});
});
};
}
Before talking about how to configure middlewares with redux, let’s talk about Enhancer. An enhancer (in redux) is in charge of ‘overriding’ the original behavior of redux. For example if we want to modify how works the dispatch (with middlewares for instance), enrich the state with extra data, add some methods in the store…
The enhancer is in charge of the creation of the store with the help of the createStore
function, then to override the store created. Its
signature is:
// We find the signature of the `createStore`
// method: function(reducer, preloadedState){}
const customEnhancer =
(createStore) => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState);
return store;
};
As you may notice, to use middlewares we need an enhancer
which is provided by redux (the only one enhancer provided by redux) which is named
applyMiddleware
:
// Transform first(second(third))(myInitialValue) with
// compose(first, second, third)(myInitialValue)
function compose(...functions) {
return functions.reduce(
(f1, f2) =>
(...args) =>
f1(f2(...args)),
);
}
const applyMiddleware =
(...middlewares) =>
(createStore) =>
(reducer, preloadedState) => {
const store = createStore(reducer, preloadedState);
const restrictedStore = {
state: store.getState(),
dispatch: () =>
console.error(
'Should not call dispatch while constructing middleware',
),
};
const chain = middlewares.map((middleware) =>
middleware(restrictedStore),
);
// We rebuild the dispatch with our middlewares
// and the original dispatch
const dispatch = compose(chain)(store.dispatch);
return {
...store,
dispatch,
};
};
Note: Perhaps you used to use the method reduce
with an accumulator initialized with a second parameter:
const myArray = [];
myArray.reduce((acc, currentValue) => {
// Do some process
}, initialValue);
If you do not give an initial value (no second paramter), the first value of your array will be taken for initial value.
The createStore
becomes:
function createStore(reducer, preloadedState, enhancer) {
// We can pass the enhancer as 2nd parameter at the place
// of preloadedState
if (
typeof preloadedState === 'function' &&
enhancer === undefined
) {
enhancer = preloadedState;
preloadedState = undefined;
}
// If we have an enhancer, let's use it to create the store
if (typeof enhancer === 'function') {
return enhancer(createStore)(reducer, preloadedState);
}
let state = preloadedState;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach((listener) => listener());
return action;
};
const subscribe = (listener) => {
listeners = [...listeners, listener];
return () => {
listeners = listeners.filter((l) => l !== listener);
};
};
const observable = () => {
const listenerSubscribe = subscribe;
return {
subscribe: (observer) => {
function observeState() {
observer.next(getState());
}
observeState();
const unsubscribe = listenerSubscribe(observeState);
return {
unsubscribe,
};
},
};
};
const replaceReducer = (newReducer) => {
reducer = newReducer;
dispatch({ type: '@@redux/REPLACE' });
};
dispatch({ type: '@@redux/INIT' });
return {
getState,
dispatch,
subscribe,
[Symbol.observable]: observable,
replaceReducer,
};
}
An now we can use our middlewares:
import loggerMiddleware from './loggerMiddleware';
import { createStore, applyMiddleware } from 'redux';
import userReducer from './userReducer';
const store = createStore(
userReducer,
applyMiddleware(loggerMiddleware),
);
We just see how is developed Redux, its implementation is quite simple to understand and so powerful to use, although it’s now possible to use the React context :)
We also see how to use it a little bit, including with ReactiveX.
In the near future we will how is implemented the library which bind Redux with React named react-redux.
You can find me on Twitter if you want to comment this post or just contact me. Feel free to buy me a coffee if you like the content and encourage me.