MobX 学习 - 04 TodoList 案例

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值