adding exercise for step 5

This commit is contained in:
Ken
2019-02-19 14:14:34 -08:00
parent a64d048706
commit be83489acc
23 changed files with 233 additions and 98 deletions

View File

@@ -1,3 +1,21 @@
# Step 2.5
# Step 2.5: Redux: Reducers
Actions and Reducers
Redux is used inside many large and complex applications because of its clarity and its predictability. It is really easy to debug and is easily extensible via its middleware architecture. In this exercise, we'll explore the heart of how Redux modifies state.
Redux uses what is called a "reducer" to modify its state. It is called this because a "reducer" is what is used inside an `Array.reduce()`.
A reducer is a **pure function** that receives some state and an action message as inputs and generates a copy of modified state as the output. Redux manages state changes mainly through reducers, and they are directly related to the UI, so for this exercise, we'll have to use jest tests to see the inner workings.
From the official documentation site:
> Reducers are just pure functions that take the previous state and an action, and return the next state. Remember to return new state objects, instead of mutating the previous state.
# Exercise
1. First, take a look at the store interface in the `exercise/src/store/index.tsx` - note that the `Store` interface has two keys: `todos` and `filter`. We'll concentrate on the `todos` which is an object where the keys are IDs and the values are of an `TodoItem` type.
2. Open `exercise/src/reducers/pureFunctions.ts` and fill in the missing body of the pure functions.
3. Open `exercise/src/reducers/index.ts` and observe how those pureFunctions are called.
4. Open `exercise/src/reducers/pureFunctions.spec.ts` and implement tests for the functions you wrote for `remove`, `complete`, and `clear`.

8
step2-05/demo/index.html Normal file
View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<body>
For this step, we look at unit testing. Run
<pre>npm test</pre>
in the command line.
</body>
</html>

View File

@@ -0,0 +1,20 @@
import { Store } from '../store';
import { addTodo, remove, complete, clear } from './pureFunctions';
export function reducer(state: Store['todos'], payload: any): Store['todos'] {
switch (payload.type) {
case 'addTodo':
return addTodo(state, payload.id, payload.label);
case 'remove':
return remove(state, payload.id);
case 'complete':
return complete(state, payload.id);
case 'clear':
return clear(state);
}
return state;
}

View File

@@ -0,0 +1,17 @@
import { addTodo } from './pureFunctions';
import { Store } from '../store';
describe('TodoApp reducers', () => {
it('can add an item', () => {
const state = <Store['todos']>{};
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();
});
});

View File

@@ -0,0 +1,32 @@
import { Store, FilterTypes } from '../store';
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
return { ...state, [id]: { label, completed: false } };
}
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 clear(state: Store['todos']) {
const newTodos = { ...state };
Object.keys(state.todos).forEach(key => {
if (state.todos[key].completed) {
delete newTodos[key];
}
});
return newTodos;
}

View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<body>
For this step, we look at unit testing. Run
<pre>npm test</pre>
in the command line.
</body>
</html>

View File

@@ -0,0 +1,9 @@
import { reducer } from './reducers';
import { createStore, compose } from 'redux';
declare var window: any;
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, {}, composeEnhancers());
console.log(store.getState());

View File

@@ -0,0 +1,20 @@
import { Store } from '../store';
import { addTodo, remove, complete, clear } from './pureFunctions';
export function reducer(state: Store['todos'], payload: any): Store['todos'] {
switch (payload.type) {
case 'addTodo':
return addTodo(state, payload.id, payload.label);
case 'remove':
return remove(state, payload.id);
case 'complete':
return complete(state, payload.id);
case 'clear':
return clear(state);
}
return state;
}

View File

@@ -0,0 +1,19 @@
import { addTodo } from './pureFunctions';
import { Store } from '../store';
describe('TodoApp reducers', () => {
it('can add an item', () => {
const state = <Store['todos']>{};
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();
});
// test remove, complete and clear
});

View File

@@ -0,0 +1,38 @@
import { Store, FilterTypes } from '../store';
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
// Write code to clone the state object while inserting a new TodoItem inside
// - the new object must be of the type TodoItem
// - the new state should be cloned using the spread syntax
// - return the new state
return state;
}
export function remove(state: Store['todos'], id: string) {
// Write code:
// - to clone the state object into new state object
// - remove and item from the new state by using the "delete" keyword
// - return the new state
return state;
}
export function complete(state: Store['todos'], id: string) {
// Write code:
// - to clone the state object into new state object
// - create a clone of the state[id] into a new item object
// - modify new state and set the id key to the value of the new item object
return state;
}
export function clear(state: Store['todos']) {
// Write code:
// - to clone the state object into new state object
// - loop through the keys of the new state object
// - remove those items inside that new state if the item is completed using the "delete" keyword
// - return the new state
return state;
}

View File

@@ -0,0 +1,14 @@
export type FilterTypes = 'all' | 'active' | 'completed';
export interface TodoItem {
label: string;
completed: boolean;
}
export interface Store {
todos: {
[id: string]: TodoItem;
};
filter: FilterTypes;
}

View File

@@ -1,6 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -1,19 +0,0 @@
import { Store } from '../store';
import { addTodo, remove, complete } from './pureFunctions';
let index = 0;
export function reducer(state: Store, payload: any): Store {
switch (payload.type) {
case 'addTodo':
return addTodo(state, payload.label);
case 'remove':
return remove(state, payload.id);
case 'complete':
return complete(state, payload.id);
}
return state;
}

View File

@@ -1,20 +0,0 @@
import { addTodo } from './pureFunctions';
import { Store } from '../store';
describe('TodoApp reducers', () => {
it('can add an item', () => {
const state = <Store>{
todos: {},
filter: 'all'
};
const newState = addTodo(state, 'item1');
const keys = Object.keys(newState.todos);
expect(newState).not.toBe(state);
expect(keys.length).toBe(1);
expect(newState.todos[keys[0]].label).toBe('item1');
expect(newState.todos[keys[0]].completed).toBeFalsy();
});
});

View File

@@ -1,34 +0,0 @@
import { Store } from '../store';
let index = 0;
export function addTodo(state: Store, label: string): Store {
const { todos } = state;
const id = index++;
return {
...state,
todos: { ...todos, [id]: { label, completed: false } }
};
}
export function remove(state: Store, id: string) {
const newTodos = { ...state.todos };
delete newTodos[id];
return {
...state,
todos: newTodos
};
}
export function complete(state: Store, id: string) {
const newTodos = { ...this.state.todos };
newTodos[id].completed = !newTodos[id].completed;
return {
...state,
todos: newTodos
};
}