context demo works
This commit is contained in:
Родитель
b91914e1d8
Коммит
4be1dcdae9
|
@ -109,7 +109,7 @@
|
|||
</li>
|
||||
<li class="Tile Tile--numbered">
|
||||
<div class="Tile-link">
|
||||
Testing with jest
|
||||
React Context
|
||||
<div class="Tile-links">
|
||||
<a target="_blank" href="./step2-04/demo/">demo</a> | <a target="_blank" href="./step2-04/exercise/">exercise</a>
|
||||
</div>
|
||||
|
|
|
@ -8363,25 +8363,25 @@
|
|||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "16.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.7.0.tgz",
|
||||
"integrity": "sha512-StCz3QY8lxTb5cl2HJxjwLFOXPIFQp+p+hxQfc8WE0QiLfCtIlKj8/+5tjjKm8uSTlAW+fCPaavGFS06V9Ar3A==",
|
||||
"version": "16.8.3",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.8.3.tgz",
|
||||
"integrity": "sha512-3UoSIsEq8yTJuSu0luO1QQWYbgGEILm+eJl2QN/VLDi7hL+EN18M3q3oVZwmVzzBJ3DkM7RMdRwBmZZ+b4IzSA==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.12.0"
|
||||
"scheduler": "^0.13.3"
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "16.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.7.0.tgz",
|
||||
"integrity": "sha512-D0Ufv1ExCAmF38P2Uh1lwpminZFRXEINJe53zRAbm4KPwSyd6DY/uDoS0Blj9jvPpn1+wivKpZYc8aAAN/nAkg==",
|
||||
"version": "16.8.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.3.tgz",
|
||||
"integrity": "sha512-ttMem9yJL4/lpItZAQ2NTFAbV7frotHk5DZEHXUOws2rMmrsvh1Na7ThGT0dTzUIl6pqTOi5tYREfL8AEna3lA==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.12.0"
|
||||
"scheduler": "^0.13.3"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
|
@ -8888,9 +8888,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.12.0.tgz",
|
||||
"integrity": "sha512-t7MBR28Akcp4Jm+QoR63XgAi9YgCUmgvDHqf5otgAj4QvdoBE4ImCX0ffehefePPG+aitiYHp0g/mW6s4Tp+dw==",
|
||||
"version": "0.13.3",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.3.tgz",
|
||||
"integrity": "sha512-UxN5QRYWtpR1egNWzJcVLk8jlegxAugswQc984lD3kU7NuobsO37/sRfbpTdBjtnD5TBNFA2Q2oLV5+UmPSmEQ==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
|
|
|
@ -63,12 +63,13 @@
|
|||
"live-server": "^1.2.1",
|
||||
"marked": "^0.6.1",
|
||||
"office-ui-fabric-react": "^6.144.0",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"react": "^16.8.3",
|
||||
"react-dom": "^16.8.3",
|
||||
"react-redux": "^6.0.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux-devtools-extension": "^2.13.8",
|
||||
"redux-starter-kit": "^0.4.3",
|
||||
"redux-react-hook": "^3.2.0",
|
||||
"redux-thunk": "^2.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ export class TodoApp extends React.Component<any, Store> {
|
|||
filter: 'all'
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filter, todos } = this.state;
|
||||
return (
|
||||
|
|
|
@ -12,7 +12,7 @@ We will solve these problems with the React Context API. The Context API consist
|
|||
|
||||
---
|
||||
|
||||
For a single component, React gives us a mental model like this:
|
||||
React represents a single component like this:
|
||||
|
||||
```
|
||||
(props) => view;
|
||||
|
@ -24,7 +24,7 @@ In a real application, these functions are composed. It looks more like this:
|
|||
|
||||
## Problems in a Complex Application
|
||||
|
||||
1. Data needs to be passed down from component to component via props. Even when some components do not need to know about some data.
|
||||
1. Data needs to be passed down from component to component via props. Even when some components do not need to know about some data. This is a problem called **props drilling**
|
||||
|
||||
2. There is a lack of coordination of changes that can happen to the data
|
||||
|
||||
|
@ -48,12 +48,11 @@ All of these props are not used, except to be passed down to a child Component,
|
|||
|
||||
## Context API
|
||||
|
||||
Let's solve the first one with the Context API. A `context` is a special way for React to share data from components to their descendant children components without having to explicitly pass down through props at every level of the tree.
|
||||
|
||||
We create a context by calling `createContext()` with some initial data:
|
||||
Let's solve these problems with the React Context API. _context_ is React's way to share data from components to their descendant children components without explicitly passing down through props at every level of the tree. React context is created by calling `createContext()` with some initial data:
|
||||
|
||||
```ts
|
||||
const TodoContext = React.createContext();
|
||||
// To create a completed empty context
|
||||
const TodoContext = React.createContext(undefined);
|
||||
```
|
||||
|
||||
Now that we have a `TodoContext` stuffed with some initial state, we will wrap `TodoApp` component with `TodoContext.Provider` so that it can provide data to all its children:
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import React from 'react';
|
||||
|
||||
// The typing forces us to put something inside createContext(); start with undefined
|
||||
export const TodoContext = React.createContext(undefined);
|
|
@ -0,0 +1,99 @@
|
|||
import React from 'react';
|
||||
import { Stack } from 'office-ui-fabric-react';
|
||||
import { TodoFooter } from './TodoFooter';
|
||||
import { TodoHeader } from './TodoHeader';
|
||||
import { TodoList } from './TodoList';
|
||||
import { Store } from '../store';
|
||||
import { TodoContext } from '../TodoContext';
|
||||
|
||||
let index = 0;
|
||||
|
||||
export class TodoApp extends React.Component<any, Store> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
todos: {},
|
||||
filter: 'all'
|
||||
};
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<TodoContext.Provider
|
||||
value={{
|
||||
...this.state,
|
||||
addTodo: this._addTodo,
|
||||
remove: this._remove,
|
||||
complete: this._complete,
|
||||
clear: this._clear,
|
||||
setFilter: this._setFilter,
|
||||
edit: this._edit
|
||||
}}
|
||||
>
|
||||
<Stack horizontalAlign="center">
|
||||
<Stack style={{ width: 400 }} gap={25}>
|
||||
<TodoHeader />
|
||||
<TodoList />
|
||||
<TodoFooter />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TodoContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
private _addTodo = label => {
|
||||
const { todos } = this.state;
|
||||
const id = index++;
|
||||
|
||||
this.setState({
|
||||
todos: { ...todos, [id]: { label } }
|
||||
});
|
||||
};
|
||||
|
||||
private _remove = id => {
|
||||
const newTodos = { ...this.state.todos };
|
||||
delete newTodos[id];
|
||||
|
||||
this.setState({
|
||||
todos: newTodos
|
||||
});
|
||||
};
|
||||
|
||||
private _complete = id => {
|
||||
const newTodos = { ...this.state.todos };
|
||||
newTodos[id].completed = !newTodos[id].completed;
|
||||
|
||||
this.setState({
|
||||
todos: newTodos
|
||||
});
|
||||
};
|
||||
|
||||
private _edit = (id, label) => {
|
||||
const newTodos = { ...this.state.todos };
|
||||
newTodos[id] = { ...newTodos[id], label };
|
||||
|
||||
this.setState({
|
||||
todos: newTodos
|
||||
});
|
||||
};
|
||||
|
||||
private _clear = () => {
|
||||
const { todos } = this.state;
|
||||
const newTodos = {};
|
||||
|
||||
Object.keys(this.state.todos).forEach(id => {
|
||||
if (!todos[id].completed) {
|
||||
newTodos[id] = todos[id];
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
todos: newTodos
|
||||
});
|
||||
};
|
||||
|
||||
private _setFilter = filter => {
|
||||
this.setState({
|
||||
filter: filter
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { DefaultButton, Stack, Text } from 'office-ui-fabric-react';
|
||||
import { TodoContext } from '../TodoContext';
|
||||
|
||||
export const TodoFooter = () => {
|
||||
const context = useContext(TodoContext);
|
||||
const itemCount = Object.keys(context.todos).filter(id => !context.todos[id].completed).length;
|
||||
|
||||
return (
|
||||
<Stack horizontal horizontalAlign="space-between">
|
||||
<Text>
|
||||
{itemCount} item{itemCount === 1 ? '' : 's'} left
|
||||
</Text>
|
||||
<DefaultButton onClick={() => context.clear()}>Clear Completed</DefaultButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
import React from 'react';
|
||||
import { Stack, Text, Pivot, PivotItem, TextField, PrimaryButton } from 'office-ui-fabric-react';
|
||||
import { FilterTypes } from '../store';
|
||||
import { TodoContext } from '../TodoContext';
|
||||
|
||||
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.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.setFilter(item.props.headerText as FilterTypes);
|
||||
};
|
||||
}
|
||||
|
||||
TodoHeader.contextType = TodoContext;
|
|
@ -0,0 +1,21 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { Stack } from 'office-ui-fabric-react';
|
||||
import { TodoListItem } from './TodoListItem';
|
||||
import { Store, FilterTypes } from '../store';
|
||||
import { TodoContext } from '../TodoContext';
|
||||
|
||||
export const TodoList = () => {
|
||||
const context = useContext(TodoContext);
|
||||
const { filter, todos } = context;
|
||||
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,76 @@
|
|||
import React from 'react';
|
||||
import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react';
|
||||
import { TodoContext } from '../TodoContext';
|
||||
|
||||
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, complete, remove } = this.context;
|
||||
|
||||
const item = todos[id];
|
||||
|
||||
return (
|
||||
<Stack horizontal verticalAlign="center" horizontalAlign="space-between">
|
||||
{!this.state.editing && (
|
||||
<>
|
||||
<Checkbox label={item.label} checked={item.completed} onChange={() => complete(id)} />
|
||||
<div>
|
||||
<IconButton iconProps={{ iconName: 'Edit' }} onClick={this.onEdit} />
|
||||
<IconButton iconProps={{ iconName: 'Cancel' }} onClick={() => 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;
|
||||
const { label } = todos[id];
|
||||
|
||||
this.setState({
|
||||
editing: true,
|
||||
editLabel: this.state.editLabel || label
|
||||
});
|
||||
};
|
||||
|
||||
private onDoneEdit = () => {
|
||||
this.context.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 = TodoContext;
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { TodoApp } from './components/TodoApp';
|
||||
import { initializeIcons } from '@uifabric/icons';
|
||||
|
||||
// Initializes the UI Fabric icons that we can use
|
||||
// Choose one from this list: https://developer.microsoft.com/en-us/fabric#/styles/icons
|
||||
initializeIcons();
|
||||
|
||||
ReactDOM.render(<TodoApp />, document.getElementById('app'));
|
|
@ -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;
|
||||
}
|
Загрузка…
Ссылка в новой задаче