From fdd6d1179184dc461fb9623841053c79fd9d6bb5 Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 26 Feb 2019 21:27:23 -0800 Subject: [PATCH] majorly overhauled 2.8 --- step2-08/README.md | 130 +++++++++++++++--- step2-08/demo/src/reducers/index.ts | 36 ++--- .../demo/src/reducers/pureFunctions.spec.ts | 22 ++- step2-08/demo/src/reducers/pureFunctions.ts | 31 ++--- step2-08/exercise/src/reducers/index.ts | 4 +- .../src/reducers/pureFunctions.spec.ts | 29 ---- .../exercise/src/reducers/pureFunctions.ts | 27 ++-- step2-09/demo/src/reducers/index.ts | 36 ++--- .../demo/src/reducers/pureFunctions.spec.ts | 22 ++- step2-09/demo/src/reducers/pureFunctions.ts | 35 ++--- step2-09/exercise/src/reducers/index.ts | 36 ++--- .../src/reducers/pureFunctions.spec.ts | 22 ++- .../exercise/src/reducers/pureFunctions.ts | 35 ++--- 13 files changed, 214 insertions(+), 251 deletions(-) delete mode 100644 step2-08/exercise/src/reducers/pureFunctions.spec.ts diff --git a/step2-08/README.md b/step2-08/README.md index 6796993..d629d02 100644 --- a/step2-08/README.md +++ b/step2-08/README.md @@ -2,11 +2,99 @@ [Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/) -Combine Reducers +The Boilerplate!! -This lesson is just a helper to make the process of writing reducers use less boilerplate code. This step briefly introduces to a world of helpers in writing Redux code. +At this point, you might asking why am I adding so much boilerplate code. 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. -Our Redux store so far has this shape, roughly: +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. I argue for two things: + +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: + +```ts +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: + +```ts +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`](https://github.com/mweststrate/immer#reducer-example) 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: + +```ts +// 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): + +```ts +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: + +```ts +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: ```js const state = { @@ -25,16 +113,20 @@ const state = { }; ``` -As the application grows in complexity, so will the shape of the store. 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: +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: ```ts -function todoReducer(state: Store['todos'] = {}, action: any) { - // reduce on the todos part of the state tree -} +// from last step, using createReducer +const todoReducer = createReducer( + {}, + { + // reduce on the todos part of the state tree + } +); -function filterReducer(state: Store['filter'] = 'all', action: any) { +const filterReducer = createReducer('all', { // reduce on the filter flag -} +}); // Then use the redux-provided combineReducers() to combine them export const reducer = combineReducers({ @@ -43,27 +135,21 @@ export const reducer = combineReducers({ }); ``` -`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. +`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. implement the `filterReducer` function with a switch / case statement - it is contrived to have a switch case for ONE condition, but serves to be a good exercise here +2. rewrite the reducer functions `todoReducers`, `filterReducers` with the help of `createReducer()` -3. replace the export reducer function with the help of the `combineReducer()` function from `redux` +3. rewrite the `reducer()` function with `combineReducer()` -# Bonus Exercise +4. open up `exercise/src/reducers/pureFunctions.ts` -The Redux team came up with `redux-starter-kit` to address a lot of boilerplate concerns. They also embed the immer library to make it nicer to write reducer functions. So, let's try out `immer`! Look at this example: https://github.com/mweststrate/immer#reducer-example - -1. import immer into the `exercise/src/reducers/pureFunction.ts` file - -2. replace the implementation of the pure functions with the help of immer's `produce()` - -3. run `npm test` in the root folder to see if it still works! - -4. look at the web app to make sure it still works! +5. rewrite all the reducers related to the todos by following instructions # Further reading diff --git a/step2-08/demo/src/reducers/index.ts b/step2-08/demo/src/reducers/index.ts index 7d3103f..243e864 100644 --- a/step2-08/demo/src/reducers/index.ts +++ b/step2-08/demo/src/reducers/index.ts @@ -1,33 +1,21 @@ import { Store } from '../store'; import { addTodo, remove, complete, clear, setFilter } from './pureFunctions'; import { combineReducers } from 'redux'; +import { createReducer } from 'redux-starter-kit'; -function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] { - switch (action.type) { - case 'addTodo': - return addTodo(state, action.id, action.label); - - case 'remove': - return remove(state, action.id); - - case 'clear': - return clear(state); - - case 'complete': - return complete(state, action.id); +const todoReducer = createReducer( + {}, + { + addTodo, + remove, + clear, + complete } +); - return state; -} - -function filterReducer(state: Store['filter'] = 'all', action: any): Store['filter'] { - switch (action.type) { - case 'setFilter': - return setFilter(state, action.filter); - } - - return state; -} +const filterReducer = createReducer('all', { + setFilter +}); export const reducer = combineReducers({ todos: todoReducer, diff --git a/step2-08/demo/src/reducers/pureFunctions.spec.ts b/step2-08/demo/src/reducers/pureFunctions.spec.ts index b3815cf..7df158a 100644 --- a/step2-08/demo/src/reducers/pureFunctions.spec.ts +++ b/step2-08/demo/src/reducers/pureFunctions.spec.ts @@ -5,25 +5,19 @@ describe('TodoApp reducers', () => { it('can add an item', () => { const state = {}; - const newState = addTodo(state, '0', 'item1'); + addTodo(state, { id: '0', label: 'item1' }); - const keys = Object.keys(newState); - - expect(newState).not.toBe(state); + const keys = Object.keys(state); expect(keys.length).toBe(1); - expect(newState[keys[0]].label).toBe('item1'); - expect(newState[keys[0]].completed).toBeFalsy(); + + expect(state['0'].label).toBe('item1'); + expect(state['0'].completed).toBeFalsy(); }); it('can complete an item', () => { const state = {}; - - let newState = addTodo(state, '0', 'item1'); - - const key = Object.keys(newState)[0]; - - newState = complete(newState, key); - - expect(newState[key].completed).toBeTruthy(); + addTodo(state, { id: '0', label: 'item1' }); + complete(state, { id: '0' }); + expect(state['0'].completed).toBeTruthy(); }); }); diff --git a/step2-08/demo/src/reducers/pureFunctions.ts b/step2-08/demo/src/reducers/pureFunctions.ts index 792e217..fa9db70 100644 --- a/step2-08/demo/src/reducers/pureFunctions.ts +++ b/step2-08/demo/src/reducers/pureFunctions.ts @@ -1,36 +1,25 @@ -import { Store, FilterTypes } from '../store'; +import { Store } from '../store'; -export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] { - return { ...state, [id]: { label, completed: false } }; +export function addTodo(state: Store['todos'], action: any) { + state[action.id] = { label: action.label, completed: false }; } -export function remove(state: Store['todos'], id: string) { - const newTodos = { ...state }; - - delete newTodos[id]; - - return newTodos; +export function remove(state: Store['todos'], action: any) { + delete state[action.id]; } -export function complete(state: Store['todos'], id: string) { - const newTodos = { ...state }; - newTodos[id].completed = !newTodos[id].completed; - - return newTodos; +export function complete(state: Store['todos'], action: any) { + state[action.id].completed = !state[action.id].completed; } export function clear(state: Store['todos']) { - const newTodos = { ...state }; - Object.keys(state).forEach(key => { if (state[key].completed) { - delete newTodos[key]; + delete state[key]; } }); - - return newTodos; } -export function setFilter(state: Store['filter'], filter: FilterTypes) { - return filter; +export function setFilter(state: Store['filter'], action: any) { + return action.filter; } diff --git a/step2-08/exercise/src/reducers/index.ts b/step2-08/exercise/src/reducers/index.ts index 6643740..cfcd9a0 100644 --- a/step2-08/exercise/src/reducers/index.ts +++ b/step2-08/exercise/src/reducers/index.ts @@ -2,6 +2,7 @@ import { Store } from '../store'; import { addTodo, remove, complete, clear, setFilter } from './pureFunctions'; import { combineReducers } from 'redux'; +// TODO: rewrite this with createReducer() function function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] { switch (action.type) { case 'addTodo': @@ -20,9 +21,8 @@ function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] { return state; } +// TODO: rewrite this with createReducer() function function filterReducer(state: Store['filter'] = 'all', action: any): Store['filter'] { - // TODO: fill in the blank here with a switch / case statement to return new filter state as specified in `action.filter` message - return state; } diff --git a/step2-08/exercise/src/reducers/pureFunctions.spec.ts b/step2-08/exercise/src/reducers/pureFunctions.spec.ts deleted file mode 100644 index b3815cf..0000000 --- a/step2-08/exercise/src/reducers/pureFunctions.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { addTodo, complete } from './pureFunctions'; -import { Store } from '../store'; - -describe('TodoApp reducers', () => { - it('can add an item', () => { - const state = {}; - - const newState = addTodo(state, '0', 'item1'); - - const keys = Object.keys(newState); - - expect(newState).not.toBe(state); - expect(keys.length).toBe(1); - expect(newState[keys[0]].label).toBe('item1'); - expect(newState[keys[0]].completed).toBeFalsy(); - }); - - it('can complete an item', () => { - const state = {}; - - let newState = addTodo(state, '0', 'item1'); - - const key = Object.keys(newState)[0]; - - newState = complete(newState, key); - - expect(newState[key].completed).toBeTruthy(); - }); -}); diff --git a/step2-08/exercise/src/reducers/pureFunctions.ts b/step2-08/exercise/src/reducers/pureFunctions.ts index c8bfb58..0392820 100644 --- a/step2-08/exercise/src/reducers/pureFunctions.ts +++ b/step2-08/exercise/src/reducers/pureFunctions.ts @@ -1,22 +1,17 @@ import { Store, FilterTypes } from '../store'; -import produce from 'immer'; - -export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] { +// TODO: for all the "todos" functions here, rewrite with mutable state +// 1. !!!IMPORTANT!!! change the signature of every function here to: +// function xyzAction(state: Store['todos'], action: any) { ... } +// +// 2. make sure NOT to return anything, just modify the "state" arg +export function addTodo(state: Store['todos'], id: string, label: string) { + // hint: state[action.id] = ... return { ...state, [id]: { label, completed: false } }; } -/* For the bonus exercise - -export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] { - return produce(state, draft => { - // TODO: implement a simple obj key assignment here - }); -} - -*/ - export function remove(state: Store['todos'], id: string) { + // hint: delete state[action.id] const newTodos = { ...state }; delete newTodos[id]; @@ -25,6 +20,7 @@ export function remove(state: Store['todos'], id: string) { } export function complete(state: Store['todos'], id: string) { + // hint: state[action.id].completed = ... const newTodos = { ...state }; newTodos[id].completed = !newTodos[id].completed; @@ -32,6 +28,7 @@ export function complete(state: Store['todos'], id: string) { } export function clear(state: Store['todos']) { + // hint: it's almost like the remove case above const newTodos = { ...state }; Object.keys(state).forEach(key => { @@ -43,6 +40,10 @@ export function clear(state: Store['todos']) { return newTodos; } +// TODO: change the setFilter() to the new immer way +// 1. change the signature of every function here to: +// function xyzAction(state: Store['todos'], action: any) { ... } +// 2. make sure to return action.filter without modifying state in this case export function setFilter(state: Store['filter'], filter: FilterTypes) { return filter; } diff --git a/step2-09/demo/src/reducers/index.ts b/step2-09/demo/src/reducers/index.ts index 7d3103f..243e864 100644 --- a/step2-09/demo/src/reducers/index.ts +++ b/step2-09/demo/src/reducers/index.ts @@ -1,33 +1,21 @@ import { Store } from '../store'; import { addTodo, remove, complete, clear, setFilter } from './pureFunctions'; import { combineReducers } from 'redux'; +import { createReducer } from 'redux-starter-kit'; -function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] { - switch (action.type) { - case 'addTodo': - return addTodo(state, action.id, action.label); - - case 'remove': - return remove(state, action.id); - - case 'clear': - return clear(state); - - case 'complete': - return complete(state, action.id); +const todoReducer = createReducer( + {}, + { + addTodo, + remove, + clear, + complete } +); - return state; -} - -function filterReducer(state: Store['filter'] = 'all', action: any): Store['filter'] { - switch (action.type) { - case 'setFilter': - return setFilter(state, action.filter); - } - - return state; -} +const filterReducer = createReducer('all', { + setFilter +}); export const reducer = combineReducers({ todos: todoReducer, diff --git a/step2-09/demo/src/reducers/pureFunctions.spec.ts b/step2-09/demo/src/reducers/pureFunctions.spec.ts index b3815cf..7df158a 100644 --- a/step2-09/demo/src/reducers/pureFunctions.spec.ts +++ b/step2-09/demo/src/reducers/pureFunctions.spec.ts @@ -5,25 +5,19 @@ describe('TodoApp reducers', () => { it('can add an item', () => { const state = {}; - const newState = addTodo(state, '0', 'item1'); + addTodo(state, { id: '0', label: 'item1' }); - const keys = Object.keys(newState); - - expect(newState).not.toBe(state); + const keys = Object.keys(state); expect(keys.length).toBe(1); - expect(newState[keys[0]].label).toBe('item1'); - expect(newState[keys[0]].completed).toBeFalsy(); + + expect(state['0'].label).toBe('item1'); + expect(state['0'].completed).toBeFalsy(); }); it('can complete an item', () => { const state = {}; - - let newState = addTodo(state, '0', 'item1'); - - const key = Object.keys(newState)[0]; - - newState = complete(newState, key); - - expect(newState[key].completed).toBeTruthy(); + addTodo(state, { id: '0', label: 'item1' }); + complete(state, { id: '0' }); + expect(state['0'].completed).toBeTruthy(); }); }); diff --git a/step2-09/demo/src/reducers/pureFunctions.ts b/step2-09/demo/src/reducers/pureFunctions.ts index 1b49648..fa9db70 100644 --- a/step2-09/demo/src/reducers/pureFunctions.ts +++ b/step2-09/demo/src/reducers/pureFunctions.ts @@ -1,40 +1,25 @@ -import { Store, FilterTypes } from '../store'; +import { Store } from '../store'; -export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] { - return { ...state, [id]: { label, completed: false } }; +export function addTodo(state: Store['todos'], action: any) { + state[action.id] = { label: action.label, completed: false }; } -export function edit(state: Store['todos'], id: string, label: string): Store['todos'] { - return { ...state, [id]: { ...state[id], label } }; +export function remove(state: Store['todos'], action: any) { + delete state[action.id]; } -export function remove(state: Store['todos'], id: string) { - const newTodos = { ...state }; - - delete newTodos[id]; - - return newTodos; -} - -export function complete(state: Store['todos'], id: string) { - const newTodos = { ...state }; - newTodos[id].completed = !newTodos[id].completed; - - return newTodos; +export function complete(state: Store['todos'], action: any) { + state[action.id].completed = !state[action.id].completed; } export function clear(state: Store['todos']) { - const newTodos = { ...state }; - Object.keys(state).forEach(key => { if (state[key].completed) { - delete newTodos[key]; + delete state[key]; } }); - - return newTodos; } -export function setFilter(state: Store['filter'], filter: FilterTypes) { - return filter; +export function setFilter(state: Store['filter'], action: any) { + return action.filter; } diff --git a/step2-09/exercise/src/reducers/index.ts b/step2-09/exercise/src/reducers/index.ts index 7d3103f..243e864 100644 --- a/step2-09/exercise/src/reducers/index.ts +++ b/step2-09/exercise/src/reducers/index.ts @@ -1,33 +1,21 @@ import { Store } from '../store'; import { addTodo, remove, complete, clear, setFilter } from './pureFunctions'; import { combineReducers } from 'redux'; +import { createReducer } from 'redux-starter-kit'; -function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] { - switch (action.type) { - case 'addTodo': - return addTodo(state, action.id, action.label); - - case 'remove': - return remove(state, action.id); - - case 'clear': - return clear(state); - - case 'complete': - return complete(state, action.id); +const todoReducer = createReducer( + {}, + { + addTodo, + remove, + clear, + complete } +); - return state; -} - -function filterReducer(state: Store['filter'] = 'all', action: any): Store['filter'] { - switch (action.type) { - case 'setFilter': - return setFilter(state, action.filter); - } - - return state; -} +const filterReducer = createReducer('all', { + setFilter +}); export const reducer = combineReducers({ todos: todoReducer, diff --git a/step2-09/exercise/src/reducers/pureFunctions.spec.ts b/step2-09/exercise/src/reducers/pureFunctions.spec.ts index b3815cf..7df158a 100644 --- a/step2-09/exercise/src/reducers/pureFunctions.spec.ts +++ b/step2-09/exercise/src/reducers/pureFunctions.spec.ts @@ -5,25 +5,19 @@ describe('TodoApp reducers', () => { it('can add an item', () => { const state = {}; - const newState = addTodo(state, '0', 'item1'); + addTodo(state, { id: '0', label: 'item1' }); - const keys = Object.keys(newState); - - expect(newState).not.toBe(state); + const keys = Object.keys(state); expect(keys.length).toBe(1); - expect(newState[keys[0]].label).toBe('item1'); - expect(newState[keys[0]].completed).toBeFalsy(); + + expect(state['0'].label).toBe('item1'); + expect(state['0'].completed).toBeFalsy(); }); it('can complete an item', () => { const state = {}; - - let newState = addTodo(state, '0', 'item1'); - - const key = Object.keys(newState)[0]; - - newState = complete(newState, key); - - expect(newState[key].completed).toBeTruthy(); + addTodo(state, { id: '0', label: 'item1' }); + complete(state, { id: '0' }); + expect(state['0'].completed).toBeTruthy(); }); }); diff --git a/step2-09/exercise/src/reducers/pureFunctions.ts b/step2-09/exercise/src/reducers/pureFunctions.ts index 1b49648..fa9db70 100644 --- a/step2-09/exercise/src/reducers/pureFunctions.ts +++ b/step2-09/exercise/src/reducers/pureFunctions.ts @@ -1,40 +1,25 @@ -import { Store, FilterTypes } from '../store'; +import { Store } from '../store'; -export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] { - return { ...state, [id]: { label, completed: false } }; +export function addTodo(state: Store['todos'], action: any) { + state[action.id] = { label: action.label, completed: false }; } -export function edit(state: Store['todos'], id: string, label: string): Store['todos'] { - return { ...state, [id]: { ...state[id], label } }; +export function remove(state: Store['todos'], action: any) { + delete state[action.id]; } -export function remove(state: Store['todos'], id: string) { - const newTodos = { ...state }; - - delete newTodos[id]; - - return newTodos; -} - -export function complete(state: Store['todos'], id: string) { - const newTodos = { ...state }; - newTodos[id].completed = !newTodos[id].completed; - - return newTodos; +export function complete(state: Store['todos'], action: any) { + state[action.id].completed = !state[action.id].completed; } export function clear(state: Store['todos']) { - const newTodos = { ...state }; - Object.keys(state).forEach(key => { if (state[key].completed) { - delete newTodos[key]; + delete state[key]; } }); - - return newTodos; } -export function setFilter(state: Store['filter'], filter: FilterTypes) { - return filter; +export function setFilter(state: Store['filter'], action: any) { + return action.filter; }