Files
frontend-bootcamp/step2-08
2019-02-26 21:35:59 -08:00
..
2019-02-26 21:27:23 -08:00
2019-02-26 21:27:23 -08:00
2019-02-26 21:35:59 -08:00

Step 2.8

Lessons | Exercise | Demo

At this point, you might asking why am I adding so much boilerplate code?!?!

It's okay. Don't be cry.

A lot of code seems to be repeated with Redux. Redux is very much function based and has a lot of opportunites for some refactoring to make it less boilerplate'ish.

I argue that part of the boilerplate is just turning what would otherwise by implicit to be explicit. This is GOOD in a large applications so that there is no magic.

However, I argue for two things to make things much better:

  1. writing against immutable data structures is hard
  2. the switch statements is cumbersome and error prone (e.g. with default case missing)

redux-starter-kit: A simple batteries-included toolset to make using Redux easier

Introducing an official library from Redux team that makes this much better. We'll start with createReducer()

createReducer(): takes away the switch statement

createReducers() simplifies things a lot! The best way illustrate what it does is with some code:

function todoReducer(state, action) {
  switch (action.type) {
    case 'addTodo':
      return addTodo(...)

    case 'remove':
      return remove(...)

    case 'clear':
      return clear(...)

    case 'complete':
      return complete(...)
  }

  return state;
}

can be rewritten as:

import {createReducer} from 'redux-starter-kit';

const todoReducer = createReducer({}, {
  addTodo: (state, action) => ...,
  remove: (state, action) => ...,
  clear: (state, action) => ...,
  complete: (state, action) => ...
})

Several important features of createReducer():

  1. it allows a more concise way of writing reducers with keys that match the action.type (using convention)

  2. handles "no match" case and returns the previous state (rather than a blank state like we had done previously)

  3. it incorporates a library called immer that allows us to write code that mutates a draft object and ultimately copies over the old snapshot with the new. Instead of writing immutable data manipulation:

// Taken from: https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns#inserting-and-removing-items-in-arrays
function insertItem(array, action) {
  return [...array.slice(0, action.index), action.item, ...array.slice(action.index)];
}

function removeItem(array, action) {
  return [...array.slice(0, action.index), ...array.slice(action.index + 1)];
}

Can become code that we write with mutable arrays (without spread syntax):

function insertItem(array, action) {
  // splice is a standard JS Array function
  array.splice(action.index, 0, action.item);
}

function removeItem(array, action) {
  array.splice(action.index, 1);
}

There are cases where you need to replace the entire state at a time (like the setFilter). Simply returning a new value without modifying the state like so:

function setFilter(state, action) {
  return action.filter;
}

combineReducer() - combining reducers

This another is demonstration of how to write reducers with less boilerplate code. We can use a built-in redux function to combineReducers. Application state shape grows usually be splitting the store. Our Redux store so far has this shape, roughly:

const state = {
  todos: {
    id0: {
      label: 'hello',
      completed: false
    },
    id1: {
      label: 'world',
      completed: true
    }
  },

  filter: 'all'
};

Currently, the store captures two separate but related ideas: the todo items and the selected filter. The reducers should follow the shape of the store. Think of reducers as part of the store itself and are responsible to update a single part of the store based on actions that they receive as a second argument. As complexity of state grows, we split these reducers:

// from last step, using createReducer
const todoReducer = createReducer(
  {},
  {
    // reduce on the todos part of the state tree
  }
);

const filterReducer = createReducer('all', {
  // reduce on the filter flag
});

// Then use the redux-provided combineReducers() to combine them
export const reducer = combineReducers({
  todos: todoReducer,
  filter: filterReducer
});

combineReducers handles the grunt-work of sending actions to each combined reducer. Therefore, when an action arrives, each reducer is given the opportunity to modify its own state tree based on the incoming action.

Exercise

Hint! This section is tricky, so all the solution is inside "demo" as usual. Feel free to copy & paste if you get stuck!!

  1. open up exercise/src/reducers/index.ts

  2. rewrite the reducer functions todoReducers, filterReducers with the help of createReducer()

  3. rewrite the reducer() function with combineReducer()

  4. open up exercise/src/reducers/pureFunctions.ts

  5. rewrite all the reducers related to the todos by following instructions

Further reading