This commit is contained in:
Ken 2019-03-02 22:24:18 -08:00
Родитель 1f4baa2ca4
Коммит 562d3bc20d
15 изменённых файлов: 268 добавлений и 178 удалений

Просмотреть файл

@ -117,7 +117,7 @@
</li>
<li class="Tile Tile--numbered">
<div class="Tile-link">
Redux: Reducers
Redux: The Store
<div class="Tile-links">
<a target="_blank" href="./step2-05/demo/">demo</a> | <a target="_blank" href="./step2-05/exercise/">exercise</a>
</div>
@ -125,28 +125,12 @@
</li>
<li class="Tile Tile--numbered">
<div class="Tile-link">
Redux: Dispatch Actions
Redux: React Binding
<div class="Tile-links">
<a target="_blank" href="./step2-06/demo/">demo</a> | <a target="_blank" href="./step2-06/exercise/">exercise</a>
</div>
</div>
</li>
<li class="Tile Tile--numbered">
<div class="Tile-link">
Redux: Connect to UI
<div class="Tile-links">
<a target="_blank" href="./step2-07/demo/">demo</a> | <a target="_blank" href="./step2-07/exercise/">exercise</a>
</div>
</div>
</li>
<li class="Tile Tile--numbered">
<div class="Tile-link">
Redux: Reduce Boilerplate
<div class="Tile-links">
<a target="_blank" href="./step2-08/demo/">demo</a> | <a target="_blank" href="./step2-08/exercise/">exercise</a>
</div>
</div>
</li>
<li class="Tile Tile--numbered">
<div class="Tile-link">
Redux: Service Calls

5
package-lock.json сгенерированный
Просмотреть файл

@ -8527,6 +8527,11 @@
"json-stringify-safe": "^5.0.1"
}
},
"redux-react-hook": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/redux-react-hook/-/redux-react-hook-3.2.0.tgz",
"integrity": "sha512-GibqTO/Cgl2nRuhw2wacyfd2Nds8pYAPU/eYuTqXZ9v01CT7s7LSwDfPdtQua7TBqE8XNjrwKZqfDyEtfXt7vQ=="
},
"redux-starter-kit": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/redux-starter-kit/-/redux-starter-kit-0.4.3.tgz",

Просмотреть файл

@ -14,10 +14,6 @@ export const todosReducer = createReducer<Store['todos']>(
},
clear(state, action) {
state[action.id].completed = !state[action.id].completed;
},
complete(state, action) {
Object.keys(state).forEach(key => {
if (state[key].completed) {
delete state[key];
@ -25,6 +21,10 @@ export const todosReducer = createReducer<Store['todos']>(
});
},
complete(state, action) {
state[action.id].completed = !state[action.id].completed;
},
edit(state, action) {
state[action.id].label = action.label;
}

Просмотреть файл

@ -1,68 +1,14 @@
# Step 2.6 - Redux: Dispatching actions and examining state (Demo)
# Step 2.6: Redux: React Binding (Demo)
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
In this step, we learn about the Redux methods `dispatch()` and `getState()`. Dispatching action messages to the store is the only means by which to instruct the reducers to modify the shared state tree.
In this step, we will continue with Redux. Step 2.5 code can be used with any other web frameworks. This step, we will hook up the Redux store with React components.
We also see how to compose reducers to make up the complete state shape.
We will demonstrate how to:
## Dispatch
1. Bind Redux store to Class Components
2. Bind Redux store to Functional Components
Given a store reference, you can [dispatch](https://redux.js.org/api/store#dispatch) an action to trigger the middleware and reducers. This changes the store and causes the view to re-render. (We'll look at how to pass the store and the dispatch function into the view later.)
## Bind Redux store to Class Components
```ts
const store = createStore(reducers);
store.dispatch(actions.addTodo('id0', 'hello world'));
```
> Important note: Dispatches generally have a "fire and forget" approach. We expect React to re-render the UI correctly of its own accord. (Rendering isn't necessarily synchronous in React! Chaining async action creators is a topic for Step 9.)
## Reducers scoped to a portion of the state tree
In general, when an application grows, so does the complexity of the state tree. In a Redux application, it is best to have reducers that deal with only a sub-portion of the tree.
In our example, we have two parts of our state: `todos` and `filter`. We will [split the reducer](https://redux.js.org/basics/reducers#splitting-reducers) to pass the todos to a `todosReducer()` function and just return `all` to the `filter` key for now. This organization helps in navigating and understanding the reducers because it matches the shape of the state one-to-one: there's a separate reducer for each key in state.
Compare this example which handles the whole state in one reducer...
```ts
// remember the shape of the store
{
todos: {
id0: {...},
id1: {...},
},
filter: 'all'
}
```
...to this one which splits it up.
```ts
function reducers(state, action) {
return {
todos: function todoReducers(state['todos'], action) {
...
},
filter: function filterReducers(state['filter'], action) {
...
}
}
}
```
With the second example, it is easy to understand which reducer changed a given part of the state.
## `getState()`
To examine the state of the store, you can call `store.getState()` to get a snapshot of the current state.
In general, you should only include serializable things in the state so that you can easily save or transfer it. You can even save this state into a browser's local storage and restore for the next boot of your application!
## Visualizing the reducer and store change
If you want a really neat UI to show what the store looks when actions are dispatched to the store, use the [Redux DevTools extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd).
This extension (available for Chrome and Firefox) is a work of genius! It lets you replay actions and step backwards to debug the current state of a Redux application. In a large enough application, this kind of debuggability is invaluable. It also helps developers who are not familiar with your application to quickly get a handle on how the state changes in response to some actions.
## Bind Redux store to Functional Components

Просмотреть файл

@ -5,11 +5,7 @@
</head>
<body class="ms-Fabric">
<div id="markdownReadme" data-src="./README.md"></div>
<div id="app">
For this step, we look at unit testing. Run
<pre>npm test</pre>
in the command line.
</div>
<div id="app"></div>
<script src="../../assets/scripts.js"></script>
</body>
</html>

Просмотреть файл

@ -4,5 +4,7 @@ export const actions = {
addTodo: (label: string) => ({ type: 'addTodo', id: uuid(), label }),
remove: (id: string) => ({ type: 'remove', id }),
complete: (id: string) => ({ type: 'complete', id }),
clear: () => ({ type: 'clear' })
clear: () => ({ type: 'clear' }),
setFilter: (filter: string) => ({ type: 'setFilter', filter }),
edit: (id: string, label: string) => ({ type: 'edit', id, label })
};

Просмотреть файл

@ -0,0 +1,17 @@
import React from 'react';
import { Stack } from 'office-ui-fabric-react';
import { TodoFooter } from './TodoFooter';
import { TodoHeader } from './TodoHeader';
import { TodoList } from './TodoList';
export const TodoApp = () => {
return (
<Stack horizontalAlign="center">
<Stack style={{ width: 400 }} gap={25}>
<TodoHeader />
<TodoList />
<TodoFooter />
</Stack>
</Stack>
);
};

Просмотреть файл

@ -0,0 +1,20 @@
import React from 'react';
import { DefaultButton, Stack, Text } from 'office-ui-fabric-react';
import { actions } from '../actions';
import { useMappedState, useDispatch } from 'redux-react-hook';
export const TodoFooter = () => {
const { todos } = useMappedState(state => state);
const dispatch = useDispatch();
const itemCount = Object.keys(todos).filter(id => !todos[id].completed).length;
return (
<Stack horizontal horizontalAlign="space-between">
<Text>
{itemCount} item{itemCount === 1 ? '' : 's'} left
</Text>
<DefaultButton onClick={() => dispatch(actions.clear())}>Clear Completed</DefaultButton>
</Stack>
);
};

Просмотреть файл

@ -0,0 +1,65 @@
import React from 'react';
import { Stack, Text, Pivot, PivotItem, TextField, PrimaryButton } from 'office-ui-fabric-react';
import { FilterTypes } from '../store';
import { actions } from '../actions';
import { StoreContext } from 'redux-react-hook';
interface TodoHeaderState {
labelInput: string;
}
export class TodoHeader extends React.Component<{}, TodoHeaderState> {
constructor(props: {}) {
super(props);
this.state = { labelInput: undefined };
}
render() {
return (
<Stack gap={10}>
<Stack horizontal horizontalAlign="center">
<Text variant="xxLarge">todos</Text>
</Stack>
<Stack horizontal gap={10}>
<Stack.Item grow>
<TextField
placeholder="What needs to be done?"
value={this.state.labelInput}
onChange={this.onChange}
styles={props => ({
...(props.focused && {
field: {
backgroundColor: '#c7e0f4'
}
})
})}
/>
</Stack.Item>
<PrimaryButton onClick={this.onAdd}>Add</PrimaryButton>
</Stack>
<Pivot onLinkClick={this.onFilter}>
<PivotItem headerText="all" />
<PivotItem headerText="active" />
<PivotItem headerText="completed" />
</Pivot>
</Stack>
);
}
private onAdd = () => {
this.context.dispatch(actions.addTodo(this.state.labelInput));
this.setState({ labelInput: undefined });
};
private onChange = (evt: React.FormEvent<HTMLInputElement>, newValue: string) => {
this.setState({ labelInput: newValue });
};
private onFilter = (item: PivotItem) => {
this.context.dispatch(actions.setFilter(item.props.headerText as FilterTypes));
};
}
TodoHeader.contextType = StoreContext;

Просмотреть файл

@ -0,0 +1,19 @@
import React from 'react';
import { Stack } from 'office-ui-fabric-react';
import { TodoListItem } from './TodoListItem';
import { useMappedState } from 'redux-react-hook';
export const TodoList = () => {
const { filter, todos } = useMappedState(state => state);
const filteredTodos = Object.keys(todos).filter(id => {
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
});
return (
<Stack gap={10}>
{filteredTodos.map(id => (
<TodoListItem key={id} id={id} />
))}
</Stack>
);
};

Просмотреть файл

@ -0,0 +1,78 @@
import React from 'react';
import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react';
import { actions } from '../actions';
import { StoreContext } from 'redux-react-hook';
interface TodoListItemProps {
id: string;
}
interface TodoListItemState {
editing: boolean;
editLabel: string;
}
export class TodoListItem extends React.Component<TodoListItemProps, TodoListItemState> {
constructor(props: TodoListItemProps) {
super(props);
this.state = { editing: false, editLabel: undefined };
}
render() {
const { id } = this.props;
const { todos } = this.context.getState();
const dispatch = this.context.dispatch;
const item = todos[id];
return (
<Stack horizontal verticalAlign="center" horizontalAlign="space-between">
{!this.state.editing && (
<>
<Checkbox label={item.label} checked={item.completed} onChange={() => dispatch(actions.complete(id))} />
<div>
<IconButton iconProps={{ iconName: 'Edit' }} onClick={this.onEdit} />
<IconButton iconProps={{ iconName: 'Cancel' }} onClick={() => dispatch(actions.remove(id))} />
</div>
</>
)}
{this.state.editing && (
<Stack.Item grow>
<Stack horizontal gap={10}>
<Stack.Item grow>
<TextField value={this.state.editLabel} onChange={this.onChange} />
</Stack.Item>
<DefaultButton onClick={this.onDoneEdit}>Save</DefaultButton>
</Stack>
</Stack.Item>
)}
</Stack>
);
}
private onEdit = () => {
const { id } = this.props;
const { todos } = this.context.getState();
const { label } = todos[id];
this.setState({
editing: true,
editLabel: this.state.editLabel || label
});
};
private onDoneEdit = () => {
this.context.dispatch(actions.edit(this.props.id, this.state.editLabel));
this.setState({
editing: false,
editLabel: undefined
});
};
private onChange = (evt: React.FormEvent<HTMLInputElement>, newValue: string) => {
this.setState({ editLabel: newValue });
};
}
TodoListItem.contextType = StoreContext;

Просмотреть файл

@ -1,13 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { reducer } from './reducers';
import { createStore } from 'redux';
import { TodoApp } from './components/TodoApp';
import { initializeIcons } from '@uifabric/icons';
import { composeWithDevTools } from 'redux-devtools-extension';
import { actions } from './actions';
import { StoreContext } from 'redux-react-hook';
const store = createStore(reducer, {}, composeWithDevTools());
console.log(store.getState());
initializeIcons();
store.dispatch(actions.addTodo('hello'));
store.dispatch(actions.addTodo('world'));
console.log(store.getState());
ReactDOM.render(
<StoreContext.Provider value={store}>
<TodoApp />
</StoreContext.Provider>,
document.getElementById('app')
);

Просмотреть файл

@ -1,27 +1,43 @@
import { Store } from '../store';
import { addTodo, remove, complete, clear } 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);
export const todosReducer = createReducer<Store['todos']>(
{},
{
addTodo(state, action) {
state[action.id] = { label: action.label, completed: false };
},
case 'remove':
return remove(state, action.id);
remove(state, action) {
delete state[action.id];
},
case 'clear':
return clear(state);
clear(state, action) {
Object.keys(state).forEach(key => {
if (state[key].completed) {
delete state[key];
}
});
},
case 'complete':
return complete(state, action.id);
complete(state, action) {
state[action.id].completed = !state[action.id].completed;
},
edit(state, action) {
state[action.id].label = action.label;
}
}
);
return state;
}
export const filterReducer = createReducer<Store['filter']>('all', {
setFilter(state, action) {
return action.filter;
}
});
export function reducer(state: Store, action: any): Store {
return {
todos: todoReducer(state.todos, action),
filter: 'all'
};
}
export const reducer = combineReducers({
todos: todosReducer,
filter: filterReducer
});

Просмотреть файл

@ -1,29 +0,0 @@
import { addTodo, complete } 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();
});
it('can complete an item', () => {
const state = <Store['todos']>{};
let newState = addTodo(state, '0', 'item1');
const key = Object.keys(newState)[0];
newState = complete(newState, key);
expect(newState[key].completed).toBeTruthy();
});
});

Просмотреть файл

@ -1,35 +0,0 @@
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) {
// Clone the todo, overriding
const newTodo = { ...state[id], completed: !state[id].completed };
return { ...state, [id]: newTodo };
}
export function clear(state: Store['todos']) {
const newTodos = { ...state };
Object.keys(state).forEach(key => {
if (state[key].completed) {
delete newTodos[key];
}
});
return newTodos;
}
export function setFilter(state: Store['filter'], filter: FilterTypes) {
return filter;
}