TodoList 案例
静态资源
组件
// src\components\Todos\TodoListView.js
import TodoHeader from "./TodoHeader"
import TodoFooter from "./TodoFooter"
import TodoView from "./TodoView"
function TodoListView() {
return (
<section className="todoapp">
<TodoHeader />
<section className="main">
<input className="toggle-all" type="checkbox" />
<ul className="todo-list">
<TodoView />
</ul>
</section>
<TodoFooter />
</section>
)
}
export default TodoListView
// src\components\Todos\TodoHeader.js
function TodoHeader() {
return (
<header className="header">
<h1>todos</h1>
<input
className="new-todo"
placeholder="What needs to be done?"
autoFocus
/>
</header>
)
}
export default TodoHeader
// src\components\Todos\TodoView.js
function TodoView() {
return (
<li>
<div className="view">
<input className="toggle" type="checkbox" />
<label>Hello MobX</label>
<button className="destroy" />
</div>
<input className="edit" />
</li>
)
}
export default TodoView
// src\components\Todos\TodoFooter.js
function TodoFooter() {
return (
<footer className="footer">
<span className="todo-count">
<strong>0</strong> item left
</span>
<ul className="filters">
<li>
<button className="selected">All</button>
</li>
<li>
<button>Active</button>
</li>
<li>
<button>Completed</button>
</li>
</ul>
<button className="clear-completed">Clear completed</button>
</footer>
)
}
export default TodoFooter
样式文件
html,body{margin:0;padding:0}button{margin:0;padding:0;border:0;background:none;font-size:100%;vertical-align:baseline;font-family:inherit;font-weight:inherit;color:inherit;-webkit-appearance:none;appearance:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{font:14px "Helvetica Neue",Helvetica,Arial,sans-serif;line-height:1.4em;background:#f5f5f5;color:#4d4d4d;min-width:230px;max-width:550px;margin:0 auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-weight:300}:focus{outline:0}.hidden{display:none}.todoapp{background:#fff;margin:130px 0 40px 0;position:relative;box-shadow:0 2px 4px 0 rgba(0,0,0,0.2),0 25px 50px 0 rgba(0,0,0,0.1)}.todoapp input::-webkit-input-placeholder{font-style:italic;font-weight:300;color:#e6e6e6}.todoapp input::-moz-placeholder{font-style:italic;font-weight:300;color:#e6e6e6}.todoapp input::input-placeholder{font-style:italic;font-weight:300;color:#e6e6e6}.todoapp h1{position:absolute;top:-155px;width:100%;font-size:100px;font-weight:100;text-align:center;color:rgba(175,47,47,0.15);-webkit-text-rendering:optimizeLegibility;-moz-text-rendering:optimizeLegibility;text-rendering:optimizeLegibility}.new-todo,.edit{position:relative;margin:0;width:100%;font-size:24px;font-family:inherit;font-weight:inherit;line-height:1.4em;border:0;color:inherit;padding:6px;border:1px solid #999;box-shadow:inset 0 -1px 5px 0 rgba(0,0,0,0.2);box-sizing:border-box;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.new-todo{padding:16px 16px 16px 60px;border:none;background:rgba(0,0,0,0.003);box-shadow:inset 0 -2px 1px rgba(0,0,0,0.03)}.main{position:relative;z-index:2;border-top:1px solid #e6e6e6}label[for="toggle-all"]{display:none}.toggle-all{position:absolute;top:-55px;left:-12px;width:60px;height:34px;text-align:center;border:none}.toggle-all:before{content:"❯";font-size:22px;color:#e6e6e6;padding:10px 27px 10px 27px}.toggle-all:checked:before{color:#737373}.todo-list{margin:0;padding:0;list-style:none}.todo-list li{position:relative;font-size:24px;border-bottom:1px solid #ededed}.todo-list li:last-child{border-bottom:none}.todo-list li.editing{border-bottom:none;padding:0}.todo-list li.editing .edit{display:block;width:506px;padding:12px 16px;margin:0 0 0 43px}.todo-list li.editing .view{display:none}.todo-list li .toggle{text-align:center;width:40px;height:auto;position:absolute;top:0;bottom:0;margin:auto 0;border:none;-webkit-appearance:none;appearance:none}.todo-list li .toggle:after{content:url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E")}.todo-list li .toggle:checked:after{content:url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E")}.todo-list li label{word-break:break-all;padding:15px 60px 15px 15px;margin-left:45px;display:block;line-height:1.2;transition:color 0.4s}.todo-list li.completed label{color:#d9d9d9;text-decoration:line-through}.todo-list li .destroy{display:none;position:absolute;top:0;right:10px;bottom:0;width:40px;height:40px;margin:auto 0;font-size:30px;color:#cc9a9a;margin-bottom:11px;transition:color 0.2s ease-out}.todo-list li .destroy:hover{color:#af5b5e}.todo-list li .destroy:after{content:"×"}.todo-list li:hover .destroy{display:block}.todo-list li .edit{display:none}.todo-list li.editing:last-child{margin-bottom:-1px}.footer{color:#777;padding:10px 15px;height:20px;text-align:center;border-top:1px solid #e6e6e6}.footer:before{content:"";position:absolute;right:0;bottom:0;left:0;height:50px;overflow:hidden;box-shadow:0 1px 1px rgba(0,0,0,0.2),0 8px 0 -3px #f6f6f6,0 9px 1px -3px rgba(0,0,0,0.2),0 16px 0 -6px #f6f6f6,0 17px 2px -6px rgba(0,0,0,0.2)}.todo-count{float:left;text-align:left}.todo-count strong{font-weight:300}.filters{margin:0;padding:0;list-style:none;position:absolute;right:0;left:0}.filters li{display:inline}.filters li button{color:inherit;padding:0 7px;text-decoration:none;border:1px solid transparent;border-radius:3px}.filters li button:hover{border-color:rgba(175,47,47,0.1)}.filters li button.selected{border-color:rgba(175,47,47,0.2)}.clear-completed,html .clear-completed:active{float:right;position:relative;line-height:20px;text-decoration:none;cursor:pointer}.clear-completed:hover{text-decoration:underline}.info{margin:65px auto 0;color:#bfbfbf;font-size:10px;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-align:center}.info p{line-height:1}.info a{color:inherit;text-decoration:none;font-weight:400}.info a:hover{text-decoration:underline}@media screen and (-webkit-min-device-pixel-ratio:0){.toggle-all,.todo-list li .toggle{background:none}.todo-list li .toggle{height:40px}.toggle-all{-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-appearance:none;appearance:none}}@media (max-width:430px){.footer{height:50px}.filters{bottom:10px}}.button{padding:6px 10px;background:#c44545;color:white;border-radius:6px;font-size:18px;margin-right:10px;cursor:pointer}.paragraph{color:#c44545;font-size:28px;font-weight:bold}
应用
// src\index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// src\App.js
import TodoListView from "./components/Todos/TodoListView"
function App() {
return (
<div>
<TodoListView />
</div>
)
}
export default App
创建 Store
// src\stores\TodoStore\TodoListStore.js
import TodoViewStore from "./TodoViewStore";
class TodoListStore {
// 待办列表
todos = []
constructor(todos) {
if (todos) this.todos = todos
}
}
export default TodoListStore
// src\stores\TodoStore\TodoViewStore.js
class TodoViewStore {
id = Math.random()
title = ''
completed = false
constructor(title) {
this.title = title
}
}
export default TodoViewStore
显示数据到视图
// src\App.js
import TodoListView from "./components/Todos/TodoListView"
import TodoListStore from './stores/TodoStore/TodoListStore'
import TodoViewStore from './stores/TodoStore/TodoViewStore'
const todoListStore = new TodoListStore([
new TodoViewStore('Hello World'),
new TodoViewStore('Hello React')
])
function App() {
return (
<div>
<TodoListView todoListStore={todoListStore} />
</div>
)
}
export default App
// src\components\Todos\TodoListView.js
import TodoHeader from "./TodoHeader"
import TodoFooter from "./TodoFooter"
import TodoView from "./TodoView"
function TodoListView({todoListStore}) {
return (
<section className="todoapp">
<TodoHeader />
<section className="main">
<input className="toggle-all" type="checkbox" />
<ul className="todo-list">
{
todoListStore.todos.map(todo => (
<TodoView todo={todo} key={todo.id} />
))
}
</ul>
</section>
<TodoFooter />
</section>
)
}
export default TodoListView
// src\components\Todos\TodoView.js
function TodoView({todo}) {
return (
<li>
<div className="view">
<input className="toggle" type="checkbox" />
<label>{todo.title}</label>
<button className="destroy" />
</div>
<input className="edit" />
</li>
)
}
export default TodoView
新增待办
- 定义新增待办方法,MobX 转化状态和方法
- 将方法传递给组件
- 配置到回车事件中
- 使用
useState
钩子函数管理 input 的 value 和 onChange - 使用
observer
包裹组件监听状态变化
// src\stores\TodoStore\TodoListStore.js
import { makeObservable, observable, action } from "mobx";
import TodoViewStore from "./TodoViewStore";
class TodoListStore {
// 待办列表
todos = []
constructor(todos) {
if (todos) this.todos = todos
// 转化状态和方法
makeObservable(this, {
todos: observable,
createTodo: action
})
}
// 新增待办
createTodo(title) {
this.todos.push(new TodoViewStore(title))
}
}
export default TodoListStore
// src\components\Todos\TodoListView.js
import TodoHeader from "./TodoHeader"
import TodoFooter from "./TodoFooter"
import TodoView from "./TodoView"
import { observer } from 'mobx-react-lite'
function TodoListView({todoListStore}) {
return (
<section className="todoapp">
{/* 将方法传递给组件 */}
<TodoHeader createTodo={title => todoListStore.createTodo(title)} />
<section className="main">
<input className="toggle-all" type="checkbox" />
<ul className="todo-list">
{
todoListStore.todos.map(todo => (
<TodoView todo={todo} key={todo.id} />
))
}
</ul>
</section>
<TodoFooter />
</section>
)
}
// 使用 `observer` 包裹组件监听状态变化
export default observer(TodoListView)
// src\components\Todos\TodoHeader.js
import { useState } from 'react'
function TodoHeader({createTodo}) {
// 使用 `useState` 钩子函数管理 input 的 value 和 onChange
const [title, setTitle] = useState('')
return (
<header className="header">
<h1>todos</h1>
<input
className="new-todo"
placeholder="What needs to be done?"
autoFocus
value={title}
onChange={event => setTitle(event.target.value)}
onKeyUp={event => {
if (event.key === 'Enter') {
// 新增待办
createTodo(event.target.value)
// 清空输入框
setTitle('')
}
}}
/>
</header>
)
}
export default TodoHeader
目前存在一个问题,createTodo
方法是作为属性传递给 TodoHeader 组件的。
类似的 Store 对象中的状态或方法,如果需要传递给嵌套较深的组件,就需要一层层传递,复杂不方便管理。
通过上下文暴露 Store
通过 React 的上下文(context)将 Store 共享到所有下级组件中,避免一层层传递。
创建上下文共享数据
// src\stores\TodoStore\TodoListStore.js
import { makeObservable, observable, action } from "mobx";
import TodoViewStore from "./TodoViewStore";
import { createContext, useContext } from 'react'
class TodoListStore {...
}
// 创建上下文
const TodoListStoreContext = createContext()
// 新建组件:通过 Provider 传递上下文中的数据
const TodoListStoreProvider = ({ store, children }) => {
return (
<TodoListStoreContext.Provider value={store}>
{children}
</TodoListStoreContext.Provider>
)
}
// 新建钩子函数:获取上下文中的数据(Store 对象)
const useTodoListStore = () => {
return useContext(TodoListStoreContext)
}
export { TodoListStore, TodoListStoreProvider, useTodoListStore }
使用上下文中的数据
// src\App.js
import TodoListView from "./components/Todos/TodoListView"
- import TodoListStore from './stores/TodoStore/TodoListStore'
+ import { TodoListStore, TodoListStoreProvider } from './stores/TodoStore/TodoListStore'
import TodoViewStore from './stores/TodoStore/TodoViewStore'
const todoListStore = new TodoListStore([
new TodoViewStore('Hello World'),
new TodoViewStore('Hello React')
])
function App() {
return (
- <div>
- <TodoListView todoListStore={todoListStore} />
- </div>
+ <TodoListStoreProvider store={todoListStore}>
+ <TodoListView />
+ </TodoListStoreProvider>
)
}
export default App
// src\components\Todos\TodoListView.js
import TodoHeader from "./TodoHeader"
import TodoFooter from "./TodoFooter"
import TodoView from "./TodoView"
import { observer } from 'mobx-react-lite'
+ import { useTodoListStore } from '../../stores/TodoStore/TodoListStore'
- function TodoListView({todoListStore}) {
+ function TodoListView() {
+ const todoListStore = useTodoListStore()
return (
<section className="todoapp">
{/* 将方法传递给组件 */}
- <TodoHeader createTodo={title => todoListStore.createTodo(title)} />
+ <TodoHeader />
<section className="main">
<input className="toggle-all" type="checkbox" />
<ul className="todo-list">
{
todoListStore.todos.map(todo => (
<TodoView todo={todo} key={todo.id} />
))
}
</ul>
</section>
<TodoFooter />
</section>
)
}
// 使用 `observer` 包裹组件监听状态变化
export default observer(TodoListView)
// src\components\Todos\TodoHeader.js
import { useState } from 'react'
+ import { useTodoListStore } from '../../stores/TodoStore/TodoListStore'
- function TodoHeader({createTodo}) {
+ function TodoHeader() {
// 使用 `useState` 钩子函数管理 input 的 value 和 onChange
const [title, setTitle] = useState('')
+ const todoListStore = useTodoListStore()
return (
<header className="header">
<h1>todos</h1>
<input
className="new-todo"
placeholder="What needs to be done?"
autoFocus
value={title}
onChange={event => setTitle(event.target.value)}
onKeyUp={event => {
if (event.key === 'Enter') {
// 新增待办
- createTodo(event.target.value)
+ todoListStore.createTodo(event.target.value)
// 清空输入框
setTitle('')
}
}}
/>
</header>
)
}
export default TodoHeader