State Management with Redux
A one day FrontEndMasters workshop, held by Steve Kinney
The original course
Introduction
Steve argues that state management is the trickiest part of creating web apps. This is what can break or make your app. If you invest time in thinking about how to structure your app, you will have to write much less code, will be easier to add new features and the UI will be performant (less re-rendering).
He also argues that while there has been some great progression in web dev community, state management is still not a solved problem.
This is of course library and framework agnostic. He will focus on Redux and Mobx.
Note, that Redux is not meant at all for API calls or side effects (this will be handled by react-redux middleware such as Redux thunk and Redux observables).
There are also other helper libraries, ie immutable.js or mori, but Steve is hesitant to add them as they can add another level of complexity that new contributors will have to learn. He mentions that Object.assign
is a pretty reliable tool to avoid mutating an object. Also the spread operator can be used ({...oringalObj, newstuff}
), that Babel will transpile to Object.assign
calls.
One big store
One of the base ideas that the entire state of your app is in one store. Of course Redux will give you a lot of helpers (e.g. combineReducers
, or mapStateToProps
, dispatching) to manage this big object.
This is also great for testing, you can serialise your state, export, recraete the state of your app and debug it. You have one single source of truth.
Some basic concepts
In Redux you dispatch actions, and have a Reducer function that takes the state and the action definition and returns a new state. Then since the state has changed, the view layer (React) will rerender the app.
Action creator functions
Most often you will not write the action object by "hand", but will use action creator functions. Also it's a good practice to save your action names as regular variables, and refer to those, to avoid spelling mistakes:
const UPDATE_USER = 'UPATE_USER'
Redux helper functions
React is very small, and most of the helper functions are not really unique to (compose
is for composing functions).
applyMiddleware: function()
bindActionCreators: function()
combineReducers: function()
compose: function()
createStore: function()
createStore
createStore
is our ability to manage the state of our app and dispatch actions. It takes a reducer function.
A reducer function usually takes a state
and an action
argument. It can be just an identity function (obj => obj
).
Then you pass it to createStore
:
const store = createStore(state => state)
Object.keys(store) // ['dispatch', 'subscribe', 'getState', 'replaceReducer']
replaceReducer
is usually used for code-splitting. The others are pretty self-explanatory.
subscribe
will fire an event every time the state changes. You would rather use React-Redux which also provides subscription.
const store = createStore(reducer)
const unsubscribe = store.subscribe(() => {console.log(store.getState().value)})
unsubscribe() // unsubscribe when no longer needs
getState
(it looks like) createStore
will run the reducer as soon as you create, so if the reducer has default value for the state, getState
will already return that. On subsequent calls of course you will have more complex state.
dispatch
: store.dispatch({foo: 'bar'})
The action has at the minimum be a POJO, but usually it has a type
, payload
and a lesser used meta
property:
{
type: 'UPDATE_SCORE',
payload: 15,
meta: {other data...}
}
The meta
property is often used for analytics.
The reducer function
It usually receives a (state
, action
) arguments and from then on you are free to write your own logic to return a new state
.
function reducer(state, action){
if(action.type === 'FOO') {
return Object.assign({}, state, {foo: true})
}
return state // the default case
}
combineReducers
It lest you break up your application into smaller, more managable parts. You provide it with an object, that maps state subtrees to reducers:
const reducer = combineReducers({
users: usersReducer,
books: booksReducer
})
Actions still flow through all of the reducers! This is crucial because it lets you handle the action in all subtrees. An action may tell to remove a post
in the blog
subtree and a comments
list from the comments
subtree that is related to the blog
> post
.
Action creators
It's very tedious to type out your action
object by hand all the time, plus small typos can "silent" bugs as the state will just "fall through" the reducer.
So instead, you will be using action creators:
const addAction = {
type: 'ADD',
payload: {
amount: 4
}
}
// now instead of the above, you will use action creator functions
function createAddAction (amount) {
return {
type: 'ADD',
payload: { amount }
}
}
// and to use
store.dispatch(createAddAction(4))
bindActionCreator
It simplifies the above pattern even further:
const dispatchAdd = bindActionCreator(createAddAction, store.dispatch)
It bins together the action creator and the dispatching, so now you can just dispatchAdd(4)
bindActionCreators
react-redux also has a bindActionCreators
function that can take several actions. It's in fact not very complicated to write such a function from scratch (see the online Workshop/bindActionCreators).
applyMiddleware
It sits between the action and the reducer. When an action is dispatched, the middleware can do other actions (think effects, async tasks, analytics) and when done pass the result to the reducer.
Simple implementation of a middleware
const logger = ({ getState }) => {
return next => action => {
console.log('Middleware', getState(), action)
return value = next(action)
return value
}
}
// then apply the middleware
const secondStore = (reducer, applyMiddleware(logger))
Typically you will use redux thunk (simple async) or redux observable (RxJS) with it.
Using Redux with React
The libraries you need to import are redux
and react-redux
.
The simple "hello world" setup:
import { connect, Provider } from 'react-redux'
import { createStore } from 'redux'
function reducer(state = {foo:'bar'}, action) {
// reduce as you want
return state
}
// an action creator
function increment() {
return { type: 'INCREMENT' }
}
// create the store
const store = createStore(reducer)
This is the basic setup for the Redux store. Now, to connect it to React, you need to
- wrap your component with the
<Provider>
- "initialise" with
connect
Here's the code, continued from above:
// ... continued from above
class Counter extends React.Component {
constructor(props) {
super(props)
}
render() {
// these are made available via the mapStateToProps and mapDispatchToProps
this.props.count // the state of the Redux store
this.props.increment // the dispatch function
return (...)
}
}
// connect to Counter
connect(mapStateToProps, mapDispatchToProps)(Counter) // see the connect explanation below
// start the React app
render(<Provider store={store}><Counter /></Provider>, document.getElementById('root'))
mapStateToProps and mapDispatchToProps
These functions define how the state and the dispatch from the Redux store are mapped to props that will be passed to the component. You can of course name them as you want, these names are just convention.
mapStateToProps
takes a state
and returns a modified version it, with only the parts needed for your component:
function mapStateToProps(state) { return state }
mapDispatchToProps
is similar but maps the dispatch method:
function mapDispatchToProps(dispatch) {
return {
inrement() { dispatch(increment()) }
}
}
You return an object, whose methods are mapped to dispatch(myActionCreator())
and connect
will automatically bin the actions to the store dispatch methods.
connect
as a Higher Order Component
The connect
method of react-redux
returns a React Higher Order Component.
So instead of the original component, now you use this in your app
const CounterContainer = connect(mapStateToProps, mapDispatchToProps)(Counter)
// use <CounterContainer /> instead of <Counter />
render(
render(<Provider store={store}><CounterContainer /></Provider>, document.getElementById('root'))
)
Again, React doesn't know anything about the state, it's all handled by Redux separately.
not passing state to React component
You can run connect(null, mapDispatchToProps)
and not pass any state to react.
A summary of steps to add a new action (decrement)
- Create the action name const (
const DECREMENT
) - Then the action creator function (
function decrementValue(){ return {type: DECREMENT}}
- Add a case for the reducer
- add it to
mapDispatchToProps
(...return {decrement() {dispatch(decrementValue)}}
{... decrement } = this.props
- add
decrement
to theonClick
Simplify the mapDispatchToProps
syntax
With the help of bindActionCreators
(the "advanced version" of bindActionCreator
(without the "s")
const mapDispatchToProps = dispatch => {
return bindActionCreators({
incrementValue, // again, these are action creator functions
decrementValue
}, dispatch)
}
Just make sure to update your props
references to use this.props.incrementValue
In the latest version on react_redux, you can also bind the action creator functions by declaring it as an object
const mapDispatchToProps = {
incrementValue,
decrementValue
}
Then connect
will automatically bind these to the store dispatch method.
On when using Redux makes sense
In smaller projects, you can do all of your state management just by using React (also see Context
and useReducer
).
If you do a strict implementation of the Redux pattern you will have lots of groups of files with this layout:
- NewItem.js
- NewItemContainer.js
- new-item-actions.js
- items-reducer.js
Sometimes this.setState
or useState
is simpler.
One rulo of thumb Brian Holt suggested is that if the state is used by more than one component then use a global store. If the state is local to your component (e.g. a "opened" state of a widget) then use useState
.
Aside on normalising your data
Normalising your data means transforming it to a different structure, one that is more adapted for your application. You can collect data from several places (APIs), but it may not be organised in a way that the UI needs it.
By normalising your data you are not only making it easier to work with, but can also save some re-renders and improve application performance. If a component only cares about the list of names, but not related data (e.g. age
), you can normalise it in a way that it only receives the names and if the age
changes it doesn't need to update.
It can save you some shouldComponentUpdate
checks.
Aside on arrays
Steve recommends to use objects instead of arrays even if your data "looks like" a list. Objects are much more performant when you have to look up data. "For every interview question, the best is to implement as a hash map or object than array".
Normalizr
A great library for doing data normalisation is Normalizr. You need to define a schema, then you run it before it's processed by Redux. You would also need to transform it again when you send it back to the server. Still, this is very efficient, since it's happening outside of React, there are no re-renders.
Normalizr will give you an object of entities and also a list (Array) of id
s.
This will allow you to add lots of optimisations to your main store. For example you can just pass a list of users that only contain the id
s, and if one user changes, via mapStateToProps
you can just update the absolut mimimum in the state received by React and so minimise the re-rendering cost.
An example to set up schemas:
import { schema, normalize } from 'normalizr'
import mylists from './mylists'
const user = new schema.Entity('users')
const card = new schema.Entity('cards', { assignTo: user })
const list = new schema.Entity('lists', { cards: [card] })
// now you can start the normalisation
const normalizedLists = normalize(mylists.lists, [list])
const normalizedUsers = normalize(mylists.users, [user])
// just map them to make them easier to use
export const lists = {
entities: normalizedLists.entities.lists,
ids: normalizedLists.result
}
export const users = {
entities: normalizedUsers.entities.lists,
ids: normalizedUsers.result
}
exponrt const cards...
This will give back the {entities: {id: 'foo', ...}, users: ['foo',...]}
structure.
end of aside on normalising data
--
Connect a State Store to a React app
A typical folder organisation would be
- actions/ (folder where you put your actions, eg. user-actions.js)
- containers/ (React containers that wrap your original components)
- reducers/
index.js (for the combined reducers)
user-reducer.js ...
Reducers
A minimal list-reducer.js
file:
// **** reducers/list-reducer.js ****
import { someDefaultState } from '../defaultstate'
function listReducer(state = someDefaultState, action){
//... do the cases...
return state
}
export default listReducer
A minimal reducers/index.js
combine reducer file:
import { combineReducers } from 'redux'
import lists from './list-reducer'
export default combineReducers({
lists
})
Add it to React
It's the same steps as before. Import createStore
, the (combine) reducer, then create the store
(createStore(reducer)
) and wrap your app with the <Provider />
.
// **** src/index.js ****
...
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import rootReducers from './reducers/index.js'
import Application from './components/Application'
// 1. create the "store"
const store = createStore(rootReducer)
// 2. then just wrap your app
ReactDOM.render(
<Provider store={store}>
<Application />
</Provider>,
document.getElementById('root')
)
Using Redux State Store in React components
This involves import
ing connect
from react-redux
, define how you want to map your store object (mapStateToProps
) and run connect
with you "original" React component:
// **** src/containers/ListContainer.js ****
import { connect } from 'react-redux'
import Lists from '../components/Lists' // this a standard React component
function mapStateToProps(state) {
return {
lists: state.lists.ids
}
}
export default connect(mapStateToProps)(Lists)
Now, you can import
your ListContainer
and use it instead of components/Lists.js
. Just need to swap the component in your React tree. The new component will now be aware of the React Store.
Using the props
with a container
You can pass a second argument to mapStateToProps
which is the props
that the component will receive. You can use it to for example to target the relevant part of the store state:
// **** src/containers/ListsContainer.js ****
...
function mapStateToProps(state, ownProps) {
return { list: state.lists.entities[ownProps.id] }
}
Then later, where you import your ListsContainer.js
file, you could for example:
function Lists({lists = []}) {
return (
<div>
{lists.map(listId => <(ListContainer id={listId} />))
</div>
)
}
Code organisation
There are several approaches as to how organise your reducers, containers and actions. There's the DUCKS pattern, where they would go into the same file, or another organisation in separate files as above. According to Steve, it doesn't really matter, as long as you are consistent within the same app.
You could also combine the standard React component and the Container into one file, since they always go together. For testing and flexibility, Steve prefers to keep them separate.
mapDispatchToProps
A typical use case is when a UI action would trigger an update of the state tree on several branches.
mapDispatchToProps
needs to return an Object as it will be combined with mapStateToProps
and other props that will be passed to the component.
an mapDispatchToProps example from the course
The goal is to create a new object from the user interaction, and dispatch it to the reducers:
import CreateItem from '../components/CreateItem' // the component you want to wrap
const defaultData = {
title: '',
description: ''
}
function mapDispatchToProps(dispatch, ownProps) {
return {
createItem(someId, someData) {
const itemId = Date.now().toString()
// could also use elements from
// ownProps, eg. ownProps.someId, but less flexible
const item = {
id: itemId,
...defaultData,
...someData
}
// can run the dispatch argument
dispatch({
type: 'ITEM_CREATE',
payload: { item, someId, cardId }
})
}
}
}
// export the Component
// we don't have to map any state to props (null)
export default connect(null, mapDispatchToProps)(CreateItem)
Then, in the original component, you could use it like so:
class CreateItem extends Component {
state = {
title: '',
description: '',
};
...
handleSubmit = event => {
event.preventDefault();
// get the createItem dispatcher from the mapDispatchToProps
const { createItem, listId } = this.props;
if (createItem) {
// call it
createItem(listId, this.state);
}
this.setState({
title: '',
description: '',
});
};
render() {
const { title, description } = this.state;
return (
<form className="CreateItem" onSubmit={this.handleSubmit}>
...
...
<input
className="CreateItem-submit"
type="submit"
value="Create New Item"
/>
</form>
);
}
}
export default CreateItem;
Use the dispatched payload in the action reducers. You can "listen" for the 'ITEM_CREATE'
action type in several reducers:
// **** src/reducers/item-reducer.js ****
import { items as defaultItems } from '../normalized-state'; // just some default definition
function itemsReducer(items = defaultItems, action) {
if (action.type === 'ITEM_CREATE') {
const { item, itemId } = action.payload;
return {
entities: { ...items.entities, [itemsId]: items },
ids: [...items.ids, itemsId],
};
}
return items;
};
export default itemsReducer;
// **** src/reducers/list-reducer.js ****
import { lists as defaultLists } from '../normalized-state';
import set from 'lodash/fp/set'; // similar to ramda setPath
const listsReducer = (lists = defaultLists, action) => {
if (action.type === 'ITEM_CREATE') {
const { itemId, listId } = action.payload;
// update the list with the payload
const items = lists.entities[listId].items.concat(itemId);
return set(['entities', listId, 'items'], items, lists);
}
return lists;
};
export default listsReducer;
Action creators and react-redux
See details on the frontendmasters course.
React-redux can automatically bind action creators with dispatch.
This means that in your code, for example in src/actions/item-actions.js
you can just simply return the action definition:
// **** src/actions/item-actions.js ****
export function createItem (listId, itemData) {
// refactored, imported from CreateItem, from above (mapDispatchToProps)
const itemId = Date.now().toString()
const item = {
id: itemId,
...defaultData,
...someData
}
// no longer need to run dispatch
// just return the action definition
return {
type: 'ITEM_CREATE',
payload: { item, someId, cardId }
}
}
Now, you would import
the action creator createItem
(above) in CreateItemContainer.js
and use it in mapDispatchToProps
.
const mapDispatchToProps = { createItem }
export default connect(null, mapDispatchToProps)(CreateItem)
<aside>
Aside on Redux Reselect
redux reselect is a memoisation library (amongs other). If you call the same function with the same arguments, it will return an already cached value.
import { createSelector } from 'reselect'
function getListEntities(state) { return state.list.entities }
// do the memoisation
const getLists = createSelector([getListEntities], lists => {
return Object.values(lists)
})
More on the frontendmasters course.
</aside>
Redux Thunk
Thunk definition
It's a function that has been returned by another function.
Redux middlewares and Redux thunk
Redux is mostly just a pattern on how to organise and update the application state. You can extend it via middlewares. When you dispatch an action, the middleware will catch it, do effects, or perform other functionalities, then later pass the action to Redux. One middleware is Redux thunk, which adds simple asynchronous actions.
A simple redux-thunk example:
export const getAllItems = () => {
return dispatch => {
API.getALL().then(items => {
dispatch({
type: 'UPDATE_ALL_ITEMS',
items
})
}
}
}
How to use middlewares React
You add them as a second argement to createStore
:
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
const store = createStore(reducer, applyMiddleware(thunk))
Now, you can create an action creator that returns a thunk, and calls dispatch
with a new action definition.
// **** src/actions.js ****
export function fetchData() {
return dispatch => {
fetch('https://example.com/api')
.then(res => res.json())
.then(res => {
dispatch(newItems(res.items))
})
}
}
function newItems(items) {
return {
type: 'NEW_ITEMS',
payload: { items }
}
}
You use it with dispatch as any other action creator function:
store.dispatch(fetchData())
Since we applied the redux-think middleware, it doesn't matter if fetchData
is a simple action creator function, returning an action definition, or thunk, making asynchronous side effects.
Now, in your reducers
you can test for NEW_ITEMS
and it will contain the asynchronously fetched data in the payload.
function items(items = [], action) {
if(action.type === 'NEW_ITEMS') {
return action.payload.items
}
return items
}
export default combineReducers({
items
})
Multiple dispatch calls in redux thunk
You can call the passed in dispatch
function several times inside your "thunk":
export function fetchData() {
return dispatch => {
// run another dispatch (simplified example)
dispatch({type: 'SET_STATUS', payload: {status: 'LOADING'}})
fetch('https://example.com/api')
.then(res => res.json())
.then(res => {
// then run dispatch again
dispatch(newItems(res.items))
})
}
}
Testing
The problem is that you cannot separate the running of the action and the dispatching.
A simple testing setup:
it('fetches items from the database', () != {
const itemsInDatabase = {
items: [{ id: 1, value: 'Cheese', packed: false }],
};
fetchMock.getOnce('/items', {
body: itemsInDatabase,
headers: { 'content-type': 'application/json' },
});
const store = mockStore({ items: [] });
return store.dispatch(actions.getItems()).then(() != {
expect(store.getItems()).toEqual({
type: GET_ALL_ITEMS,
items: itemsInDatabase
});
});
});
Observables with React
Observables are a stream of values over time. The stream can be stopped, paused, cancelled, batched, zipped with other streams, etc. An example could be the stream of mouse movements.
Redux Observables is a combination of RxJS and Redux. Side effects are managed via epics.
What is an epic?
A function that takes a stream of all actions dispatched and returns a stream of new actions to dispatch.
So it "sits" between two Redux Actions. It receives actions, does side effects then dispatches other actions. Note, that actions also go through the regular reducers, not only the EPIC. A dispatched action will both go through the epic and the standard Redux reducer.
A basic example:
function pingPongEgic(action$, store) {
return action$.ofType('PING')
.map(action => ({ type: 'PONG'}))
}
Adding redux-observer to a React project
As there are root reducers there are also root epics.
// **** src/index.js ****
import { createStore, applyMiddleware } from 'redux'
import { createEpicMiddleware } from 'redux-observable'
import rootEpic from './fetch-items-epic.js' // see the root epic implementation below
const epicMiddleWare = createEpicMiddleware();
const store = createStore(reducer, applyMiddleware(epicMiddleware))
// need to run it similar to a generators runner (Redux Saga)
epicMIddleware.run(rootEpic)
Now create the root epic:
// **** src/fetch-items-epic.js ****
import { ajax } from 'rxjs/ajax'
import { ofType } form 'redux-observable'
import { map, mergeMap } from 'rxjs/operators'
// import some action types and action creators form the regular actions.js:
import { FETCH_ITEMS, fetchItemsFulfilled } from './actions.js'
// now we can create our epic
function fetchItemsEpic (action$) {
return action$.pipe(
ofType(FETCH_ITEMS),
// mergeMap is needed, because we pipe (and so nest)
// the ajax.getJSON call
mergeMap(action => ajax.getJSON('http:example.com/api/' + action.payload.searchTerm)
// "pipe into", the regular action creator
.pipe(map(res => fetchItemsFulfilled(res.result)))
)
)
}
export default fetchItemsEpic;
By chaining different RxJS, you can have lot of flexibility in your app (throttling, debouncing, canceling, etc).
In your components
In your actions.js
file, you would create a fetchItems
actions creator:
export function fetchItems(searchTerm) {
return {
type: 'FETCH_ITEMS',
payload: { searchTerm }
}
}
Then, you can call this action creator function to trigger the epic:
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { fetchItems } from './actions';
const FetchItems = ({ fetchItems }) => {
const [value, setValue] = useState('');
const handleChange = (event) => {
const newValue = event.target.value;
setValue(newValue);
// now can call the action creators:
if (fetchItems) fetchItems(newValue);
};
return (
<input
onChange={handleChange}
placeholder="Search Here"
type="search"
value={value}
/>
);
};
// map to props...
export default connect(
null,
{ fetchItems },
)(FetchItems);
See the complete solution from the FrontEndMasters course on GitHub
A few RxJS methods
You can use them inside the pipe
for example.
tap
Like the equivalent in Ramda, it performs a side effect and returns the same argument passed to it.
function fetchItemsEpic (action$) {
return action$.pipe(
ofType(FETCH_ITEMS),
mergeMap(action => ajax.getJSON('http:example.com/api/' + action.payload.searchTerm)
.pipe(
// add tap:
tap(v => console.log(v)),
map(res => fetchItemsFulfilled(res.result))
)
)
)
}
Cancel streams with takeUntil
The goal if the code below is to cancel the fetching of the items, as soon as a new FETCH_ITEMS
action comes in. So for example, if it's an autocomplete field, stop fetching the items, as soon as the user types in the search field.
function fetchItemsEpic (action$) {
return action$.pipe(
ofType(FETCH_ITEMS),
mergeMap(action => ajax.getJSON('http:example.com/api/' + action.payload.searchTerm)
.pipe(
map(res => fetchItemsFulfilled(res.result)),
// add takeUntil:
takeUntil(action$.pipe(
// this will cancel the fetching
ofType(FETCH_ITEMS)
))
)
)
)
}
THE END