mirror of
https://github.com/microsoft/frontend-bootcamp.git
synced 2026-01-26 14:56:42 +08:00
adding exercise for step 5
This commit is contained in:
@@ -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
8
step2-05/demo/index.html
Normal 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>
|
||||
20
step2-05/demo/src/reducers/index.ts
Normal file
20
step2-05/demo/src/reducers/index.ts
Normal 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;
|
||||
}
|
||||
17
step2-05/demo/src/reducers/pureFunctions.spec.ts
Normal file
17
step2-05/demo/src/reducers/pureFunctions.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
32
step2-05/demo/src/reducers/pureFunctions.ts
Normal file
32
step2-05/demo/src/reducers/pureFunctions.ts
Normal 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;
|
||||
}
|
||||
8
step2-05/exercise/index.html
Normal file
8
step2-05/exercise/index.html
Normal 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>
|
||||
9
step2-05/exercise/src/index.tsx
Normal file
9
step2-05/exercise/src/index.tsx
Normal 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());
|
||||
20
step2-05/exercise/src/reducers/index.ts
Normal file
20
step2-05/exercise/src/reducers/index.ts
Normal 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;
|
||||
}
|
||||
19
step2-05/exercise/src/reducers/pureFunctions.spec.ts
Normal file
19
step2-05/exercise/src/reducers/pureFunctions.spec.ts
Normal 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
|
||||
});
|
||||
38
step2-05/exercise/src/reducers/pureFunctions.ts
Normal file
38
step2-05/exercise/src/reducers/pureFunctions.ts
Normal 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;
|
||||
}
|
||||
14
step2-05/exercise/src/store/index.ts
Normal file
14
step2-05/exercise/src/store/index.ts
Normal 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;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user