State Management with Redux

In notebook:
Work Notes
Created at:
2019-09-04
Updated:
2019-09-18
Tags:
React libraries Fundamentals

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

  1. wrap your component with the <Provider>
  2. "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)

  1. Create the action name const (const DECREMENT)
  2. Then the action creator function (function decrementValue(){ return {type: DECREMENT}}
  3. Add a case for the reducer
  4. add it to mapDispatchToProps (... return {decrement() {dispatch(decrementValue)}}
  5. {... decrement } = this.props
  6. add decrement to the onClick

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 ids.

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 ids, 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 importing 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