image.png

1. React-Redux 介绍

目标: 能够说出为什么需要使用 react-redux

知识点:

react-redux 库是 Redux 官方提供的 React 绑定库

  1. 为什么要使用 React-Redux 绑定库?

答:React 和 Redux 是两个独立的库,两者之间职责独立。因此,为了实现在 React 中使用 Redux 进行状态管理 ,就需要一种机制,将这两个独立的库关联在一起。这时候就用到 React-Redux 这个绑定库了。

  1. React-Redux 的作用

为 React 接入 Redux,实现在 React 中使用 Redux 进行状态管理

总结:

react-redux 是为了让 React 接入 Redux,官方提供的 React 绑定库

2. React-Redux 基本使用

目标:使用 react-redux 库在 react 中使用 redux 管理状态

知识点:

react-redux 文档

  1. react-redux 的使用分为两大步:
  • 将 react-redux 进行全局配置(只需要配置一次)
  • 组件接入(获取状态或修改状态)
  1. 使用步骤
  • 安装 react-redux:yarn add react-redux
  • 从 react-redux 中导入 Provider 组件
  • 导入创建好的 redux 仓库
  • 使用 Provider 包裹整个应用
  • 将导入的 store 设置为 Provider 的 store 属性值

落地代码:

  1. store.js ```jsx import { createStore } from ‘redux’ // 创建 store 仓库 const store = createStore(reducer)

// 操作数据 function reducer(state = 10, action) { switch (action.type) { case ‘increment’: return state + 1 default: return state } }

// 分发 action store.dispatch({ type: ‘increment’ })

export default store

  1. 2. index.js 入口文件
  2. ```jsx
  3. import ReactDOM from 'react-dom'
  4. import App from './09-redux/02-react-redux'
  5. // 从 react-redux 中导入 Provider 进行提供数据
  6. import { Provider } from 'react-redux'
  7. // 导入 store.js
  8. import store from './store'
  9. ReactDOM.render((
  10. // 使用 Provider 包裹整个应用
  11. // 将导入的 store 设置为 Provider 的 store 属性值
  12. <Provider store={store}>
  13. <App />
  14. </Provider>
  15. ), document.getElementById('root'))

小结:

  • React-Redux 的使用流程

3. React-Redux-获取状态

目标:能够使用 useSelector hook 获取redux中共享的状态

知识点:

使用 useSelector hook 获取redux中共享的状态

  1. useSelector:获取 Redux 提供的状态数据
  2. 参数:selector 函数,用于从 Redux 状态中筛选出需要的状态数据并返回
  3. 返回值:筛选出的状态 ```jsx import { useSelector } from ‘react-redux’

// 计数器案例中,Redux 中的状态是数值,所以,可以直接返回 state 本身 const count = useSelector(state => state)

// 比如,Redux 中的状态是个对象,就可以: const list = useSelector(state => state.list)

  1. **落地代码:**
  2. ```jsx
  3. // 导入 useSelector hook
  4. import { useSelector } from 'react-redux'
  5. function App() {
  6. // 调用 useSelector,传入 selector 函数
  7. const data = useSelector((state) => state)
  8. console.log(data)
  9. return null
  10. }
  11. export default App

总结:

如何获取 redux 共享的状态:使用 useSelector hook 获取redux中共享的状态

4. React-Redux-修改状态

目标:能够使用 useDispatch hook 修改 redux 中共享的状态

知识点:

使用 useDispatch hook 修改 redux 中共享的状态

  1. useDispatch:拿到 dispatch 函数,分发 action,修改 redux 中的状态数据
  2. 调用 useDispatch hook,返回 dispatch 函数
  3. 调用返回的 dispatch 函数传入 action,来分发动作即可 ```jsx import { useDispatch } from ‘react-redux’

// 调用 useDispatch hook,拿到 dispatch 函数 const dispatch = useDispatch()

// 调用 dispatch 传入 action,来分发动作 dispatch(action)

  1. **落地代码:**
  2. 1. store.js
  3. ```jsx
  4. import * as types from '../actionTypes'
  5. const reducer = (count = 0, action) => {
  6. switch (action.type) {
  7. case types.increment:
  8. return count + action.payload
  9. case types.decrement:
  10. return count - action.payload
  11. default:
  12. return count
  13. }
  14. }
  15. export default reducer
  1. App.js ```javascript import { useSelector, useDispatch } from ‘react-redux’ import * as types from ‘./store/actionTypes’

function App() { // 调用 useSelector,传入 selector 函数 const data = useSelector((state) => state) const dispatch = useDispatch()

return (

{data}

) }

export default App

  1. ** 总结:**
  2. 如何修改 redux 中共享的状态:使用 useDispatch hook 修改 redux 中共享的状态
  3. <a name="j0wVO"></a>
  4. ## 5. 理解 Redux 数据流
  5. > 目标:能够说出 redux 数据流动过程
  6. **知识点:**<br />![redux.gif](https://cdn.nlark.com/yuque/0/2022/gif/147664/1642412784711-8a8906f8-008b-4e76-b190-94de002f8ef9.gif#clientId=u93ae0495-a42b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=500&id=u34ecc7e5&margin=%5Bobject%20Object%5D&name=redux.gif&originHeight=1080&originWidth=1440&originalType=binary&ratio=1&rotation=0&showTitle=false&size=3927678&status=done&style=none&taskId=uf66f3aee-eca9-4cb3-953e-8923bb14deb&title=&width=666)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/147664/1642323983177-75b5350d-7b6e-413e-87d8-fcb6cad00c9f.png#clientId=u323a7ccf-5db9-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=uf835442a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=651&originWidth=1764&originalType=binary&ratio=1&rotation=0&showTitle=false&size=318442&status=done&style=stroke&taskId=ufa14e627-9b2f-43d7-9ac0-44463a35a21&title=)
  7. 1. Store 中创建数据,组件(视图)使用 Store 中数据进行页面的渲染
  8. 1. 组件(视图)想修改数据,需要通过事件触发 ActionAction 描述做了什么事,即行为,
  9. 1. Action 通过 Dispatcher 进行分发,分发到 Store
  10. 1. Store 接收到分发的 Action 以后,使用 reducer 进行数据的变更或其他处理
  11. 1. Store 的数据发生变更以后,驱动视图更新
  12. ** 总结:**
  13. - 任何一个组件都可以直接接入 Redux,也就是可以直接:1 修改 Redux 状态,2 接收 Redux 状态
  14. - 只要 Redux 中的状态改变了,所有接收该状态的组件都会收到通知,也就是可以获取到最新的 Redux 状态
  15. - 这样的话,两个组件不管隔得多远,都可以直接通讯了
  16. <a name="wBjgm"></a>
  17. ## 6. Redux应用-代码结构
  18. > 目录:能够组织 redux 的代码结构
  19. **知识点:**
  20. 在使用 Redux 进行项目开发时,不会将 action/reducer/store 都放在同一个文件中,而是会进行拆分,可以按照以下结构,来组织 Redux 的代码:

|-store —- 在 src 目录中创建,用于存放 Redux 相关的代码 |- actions —- 存放所有的 action |- reducers —- 存放所有的 reducer |- index.js —- redux 的入口文件,用来创建 store

  1. <a name="DMwPN"></a>
  2. ## 7. Redux应用-ActionType的使用
  3. > 目标:能够知道为什么要抽离 action type
  4. **知识点:**
  5. 1. Action Type 指的是:action 对象中 type 属性的值
  6. - type 属性的值会在多个地方使用 action type,比如:action 对象、reducer 函数、dispatch(action) 等
  7. - 如果要更新一个 type 属性值,需要更改多个地方,维护不方便,容易发生错误
  8. 3. 解决方案:集中处理 action type,保持项目中 action type 的一致性
  9. 2. action type 的值采用:'domain/action' (功能/动作) 形式,进行分类处理,比如:
  10. - 计数器: 'counter/increment' → 表示 Counter 功能中的 increment 动作
  11. - 登 录: 'login/getCode' → 表示登录获取验证码的动作
  12. - 个人资料: 'profile/get' → 表示获取个人资料
  13. **实现步骤:**
  14. 1. 在 store 目录中创建 actionTypes 目录或者 constants 目录,集中处理
  15. 1. 创建常量来存储 action type,并导出
  16. 1. 将项目中用到 action type 的地方替换为这些常量,从而保持项目中 action type 的一致性
  17. **落地代码:**
  18. 1. actionTypes/index.js
  19. ```jsx
  20. const increment = 'counter/increment'
  21. const decrement = 'counter/decrement'
  22. export { increment, decrement }
  1. acitons/index.js ```javascript import * as types from ‘../acitonTypes’

const reducer = (state, action) => { switch (action.type) { case types.increment: return state + 1 case types.decrement: return state - action.payload default: return state } }

export default reducer

  1. 3. App.js
  2. ```javascript
  3. import { useSelector, useDispatch } from 'react-redux'
  4. import * as types from './store/actionTypes'
  5. // const increment = (payload) => ({ type: types.increment, payload })
  6. // const decrement = (payload) => ({ type: types.decrement, payload })
  7. function App() {
  8. // 调用 useSelector,传入 selector 函数
  9. const data = useSelector((state) => state)
  10. const dispatch = useDispatch()
  11. return (
  12. <div>
  13. <p>{data}</p>
  14. <button onClick={() => dispatch({ type: types.increment, payload: 1 })}>修改数据 + 1</button>
  15. <button onClick={() => dispatch({ type: types.decrement, payload: 1 })}>修改数据 - 1</button>
  16. {/*
  17. <button onClick={() => dispatch(increment(1))}>修改数据 + 1</button>
  18. <button onClick={() => dispatch(decrement(1))}>修改数据 - 1</button>
  19. */}
  20. </div>
  21. )
  22. }
  23. export default App

8. Redux应用-Reducer的分离与合并

目标:能够合并 redux 的多个 reducer 为一个 根 reducer

知识点:

问题:随着项目功能变得越来越复杂,需要 Redux 管理的状态也会越来越多

解决方案:有两种方式来处理状态的更新:

  1. 使用一个 reducer:处理项目中所有状态的更新
  2. 使用多个 reducer:按照项目功能划分,每个功能使用一个 reducer 来处理该功能的状态更新

推荐:使用多个 reducer(第二种方案)

  1. 每个 reducer 处理的状态更单一,职责更明确,项目中会有多个 reducer
  2. 但是 store 只能接收一个 reducer,因此,需要将多个 reducer 合并为一根 reducer,才能传递给 store

如何合并?

  1. 使用 Redux 中的 combineReducers 函数
  2. 注意:合并后,Redux 的状态会变为一个对象,对象的结构与 combineReducers 函数的参数结构相同

    1. 比如,此时 Redux 状态为:{ a aReducer 处理的状态, b bReducer 处理的状态 }
  1. import { combineReducers } from 'redux'
  2. // 计数器案例,状态默认值为:0
  3. const aReducer = (state = 0, action) => {}
  4. // Todos 案例,状态默认值为:[]
  5. const bReducer = (state = [], action) => {}
  6. // 合并多个 reducer 为一个 根reducer
  7. const rootReducer = combineReducers({
  8. a: aReducer,
  9. b: bReducer
  10. })
  11. // 创建 store 时,传入 根reducer
  12. const store = createStore(rootReducer)
  13. // 此时,合并后的 redux 状态: { a: 0, b: [] }

注意:

  1. 虽然在使用 combineReducers 以后,整个 Redux 应用的状态变为了对象,但是,对于每个 reducer 来说,每个 reducer 只负责整个状态中的某一个值

    • 也就是:每个reducer只负责自己要处理的状态
    • 举例:
      • 登录功能:loginReducer 处理的状态只应该是跟登录相关的状态
      • 个人资料:profileReducer 处理的状态只应该是跟个人资料相关的状态
  2. 合并 reducer 后,redux 处理方式:

  • 只要合并了 reducer,不管分发什么 action,所有的 reducer 都会执行一次。
  • 各个 reducer 在执行的时候,能处理这个 action 就处理,处理不了就直接返回上一次的状态。
  • 所以,分发的某一个 action 就只能被某一个 reducer 来处理,最终只会修改这个reducer 要处理的状态,
  • 最终的表现就是:分发了 action,只修改了 redux 中这个 action 对应的状态

9. Redux应用-redux管理哪些状态

目标:能够知道什么状态可以放在 redux 中管理

知识点:

状态的处理有两种方式:

  1. 不推荐用法:将所有的状态全部放到 redux 中,由 redux 管理
  2. 推荐的用法:只将某些状态数据放在 redux 中,其他数据可以放在组件中,
  • 如果一个状态,只在某个组件中使用(比如,表单项的值),推荐:放在组件中
  • 需要放到 redux 中的状态:
    • 在多个组件中都要使用的数据【涉及组件通讯】
    • 通过 ajax 请求获取到的接口数据【涉及到请求相关逻辑代码放在哪的问题】

10. 综合案例-todomvc

10.1 案例结构搭建

目标:能够根据模板搭建案例结构

实现步骤:

使用准备好的模板内容,搭建项目,并分析案例的中组件的层级结构

  1. - App
  2. - TodoHeader
  3. - TodoMain
  4. - TodoItem
  5. - TodoFooter

10.2 配置 Redux 基本结构

目标:能够在 todomvc 案例中配置 Redux

实现步骤:

  1. 安装 redux:yarn add redux
  2. 在 src 目录中创建 store 文件夹
  3. 在 store 目录中创建 actions、reducers、actionTypes 目录以及 index.js 文件
  4. 在 reducers 目录中新建 todos.js 和 index.js 文件
  5. 在 todos.js 中创建一个基本的 reducer 并导出
  6. 在 reducers/index.js 中创建根 reducer 并导出
  7. 在 store/index.js 中,导入根 reducer 并创建 store 然后导出

落地代码:

store/reducers/todos.js 中:

  1. // 默认值
  2. const initialState = [
  3. { id: 1, text: '吃饭', done: true },
  4. { id: 2, text: '学习', done: false },
  5. { id: 3, text: '睡觉', done: true }
  6. ]
  7. const todos = (state = initialState, action) => {
  8. return state
  9. }
  10. export default todos

store/reducers/index.js 中:

  1. import { combineReducers } from 'redux'
  2. // 导入 reducer
  3. import todos from './todos'
  4. // 合并 reducer
  5. const rootReducer = combineReducers({
  6. todos
  7. })
  8. export default rootReducer

store/index.js 中:

  1. import { createStore } from 'redux'
  2. // 导入 根 reducer
  3. import rootReducer from './reducers'
  4. // 创建 store
  5. const store = createStore(rootReducer)
  6. export default store

10.3 配置 React-Redux

目标:能够在 todomvc 案例中配置 react-redux

实现步骤:

  1. 安装 react-redux:yarn add react-redux
  2. 在 src/index.js 中,导入 Provider 组件
  3. 在 src/index.js 中,导入创建好的 store
  4. 使用 Provider 包裹 App 组件,并设置其 store 属性

落地代码:

  1. import ReactDOM from 'react-dom'
  2. import './styles/base.css'
  3. import './styles/index.css'
  4. import App from './App'
  5. // 导入 Provider 组件用来提供数据
  6. import { Provider } from 'react-redux'
  7. // 导入 store
  8. import store from './store'
  9. ReactDOM.render(
  10. <Provider store={store}>
  11. <App />
  12. </Provider>,
  13. document.querySelector('#root'))

10.4 渲染任务列表

目标:能够渲染任务列表

image.png

实现步骤:

  1. 在 TodoMain 组件中导入 useSelector hook
  2. 调用 useSelector 拿到 todos 状态,也就是任务列表数据
  3. 遍历任务列表数据,将每个任务项数据传递给 TodoItem 组件
  4. 在 TodoItem 组件中,拿到数据并渲染(暂时不考虑选中问题)

落地代码:

TodoMain.js 中:

  1. import { TodoItem } from './TodoItem'
  2. + import { useSelector } from 'react-redux'
  3. export const TodoMain = () => {
  4. + // 获取状态
  5. + const list = useSelector(state => state.todos)
  6. return (
  7. <section className="main">
  8. <input id="toggle-all" className="toggle-all" type="checkbox" />
  9. <label htmlFor="toggle-all">Mark all as complete</label>
  10. <ul className="todo-list">
  11. + {
  12. + list.map(item => (
  13. + <TodoItem key={item.id} {...item} />
  14. + ))
  15. + }
  16. </ul>
  17. </section>
  18. )
  19. }

TodoItem.js 中:

  1. + import classNames from 'classnames'
  2. export const TodoItem = ({ id, text, done }) => {
  3. return (
  4. // 编辑时,添加类名:editing
  5. + <li className={classNames({ completed: done })}>
  6. <div className="view">
  7. <input className="toggle" type="checkbox" />
  8. + <label>{text}</label>
  9. <button className="destroy"></button>
  10. </div>
  11. <input className="edit" />
  12. </li>
  13. )
  14. }

10.5 渲染未完成任务数量

目标:能够渲染未完成任务数量

image.png

功能分析:

  1. 问题:实现该功能,是添加一个新的状态,还是用当前已有的状态?

  2. 回答:看一下要用到的这个数据,能不能直接根据现有的状态得到,如果能直接用现有的数据即可;否则,就要创建新的状态了,比如,现在要用的未完成任务数量,可以直接从 todos 任务列表数据中过滤得到,所以,直接用当前数据即可

实现步骤:

直接从 todos 任务列表数据中过滤得到未完成的任务(done 状态代表任务是否完成,false 代表未完成)

  1. 在 TodoFooter 组件中导入 useSelector hook
  2. 调用 useSelector 拿到 todos 状态,也就是任务列表数据
  3. 根据任务列表数据,过滤出未完成任务并拿到其长度,然后渲染

落地代码:

  1. import { useSelector } from 'react-redux'
  2. export const TodoFooter = () => {
  3. // 获取到未完成任务数量
  4. const leftCount = useSelector(state => state.todos.filter(item => !item.done).length)
  5. return (
  6. <footer className="footer">
  7. <span className="todo-count">
  8. <strong>{leftCount}</strong> item left
  9. </span>
  10. <ul className="filters">
  11. {/* ...... */}
  12. </ul>
  13. <button className="clear-completed">Clear completed</button>
  14. </footer>
  15. )
  16. }

10.6 删除任务

目标:能够实现删除任务功能

image.png

实现步骤:

  1. 给删除按钮绑定点击事件
  2. 在点击事件中分发删除任务的 action
  3. 创建 actionTypes/todos.js 文件,导出删除任务的 action type
  4. 创建 actions/todos.js 文件,创建删除任务的 action
  5. 在 reducers/todos.js 中,根据 action 类型删除任务

落地代码:

TodoItem.js 中:

  1. import classNames from 'classnames'
  2. + // 导入 useDispatch 分发 action
  3. + import { useDispatch } from 'react-redux'
  4. + // 导入 action-creator
  5. + import { delTodo } from '../store/actions/todos'
  6. export const TodoItem = ({ id, text, done }) => {
  7. + const dispatch = useDispatch()
  8. return (
  9. // 编辑时,添加类名:editing
  10. <li className={classNames({ completed: done })}>
  11. <div className="view">
  12. <input className="toggle" type="checkbox" />
  13. <label>{text}</label>
  14. + <button className="destroy" onClick={() => dispatch(delTodo(id))}></button>
  15. </div>
  16. <input className="edit" />
  17. </li>
  18. )
  19. }

actionTypes/todos.js 中:

  1. // 删除任务
  2. export const DEL_TODO = 'todos/del'

actions/todos.js 中:

  1. // 导入创建好的 action type
  2. import * as types from '../actionTypes/todos'
  3. // 删除任务
  4. export const delTodo = id => ({
  5. type: types.DEL_TODO,
  6. payload: id
  7. })

reducers/todos.js 中:

  1. import * as types from '../actionTypes/todos'
  2. // 默认值
  3. const initialState = [
  4. { id: 1, text: '吃饭', done: true },
  5. { id: 2, text: '学习', done: false },
  6. { id: 3, text: '睡觉', done: true }
  7. ]
  8. const todos = (state = initialState, action) => {
  9. switch (action.type) {
  10. case types.DEL_TODO:
  11. return state.filter(item => item.id !== action.payload)
  12. default:
  13. return state
  14. }
  15. }
  16. export default todos

10.7 切换任务完成状态

目标:能够实现切换任务完成状态

image.png

实现步骤:

  1. 为 TodoItem 组件中的 checkbox 添加 checked 值为:props.done 并为其绑定 change 事件
  2. 在 change 事件中分发切换任务完成状态的 action
  3. 在 actionTypes/todos.js 中,创建切换任务的 action type 并导出
  4. 在 actions/todos.js 文件,创建切换任务的 action 并导出
  5. 在 reducers/todos.js 中,根据 action 类型切换任务完成状态

落地代码:

TodoItem.js 中:

  1. // 导入 action-creator
  2. + import { delTodo, toggleTodo } from '../store/actions/todos'
  3. export const TodoItem = ({ id, text, done }) => {
  4. const dispatch = useDispatch()
  5. return (
  6. // 编辑时,添加类名:editing
  7. <li className={classNames({ completed: done })}>
  8. <div className="view">
  9. + <input className="toggle" type="checkbox" checked={done} onChange={() => dispatch(toggleTodo(id))} />
  10. <label>{text}</label>
  11. <button className="destroy" onClick={() => dispatch(delTodo(id))}></button>
  12. </div>
  13. <input className="edit" />
  14. </li>
  15. )
  16. }

actionTypes/todos.js 中:

  1. // 切换任务
  2. export const TOGGLE_TODO = 'todos/toggle'

actions/todos.js 中:

  1. // 切换任务的完成状态
  2. export const toggleTodo = id => ({
  3. type: types.TOGGLE_TODO,
  4. payload: id
  5. })

reducers/todos.js 中:

  1. // 切换任务
  2. case types.TOGGLE_TODO:
  3. return state.map(item => {
  4. if (item.id === action.payload) {
  5. return {
  6. ...item,
  7. done: !item.done
  8. }
  9. }
  10. return item
  11. })

10.8 添加任务

目标:能够实现添加任务

image.png

实现步骤:

思考:控制文本框的状态,应该放在 redux 中,还是放在组件中?
回答:组件中

  1. 在 TodoHeader 组件中通过受控组件获取文本框的值
  2. 给 input 绑定 keyDown 事件,在事件处理程序中判断按键是不是回车
  3. 如果不是,直接 return 不执行添加操作
  4. 如果是,分发添加任务的 action
  5. 分别添加添加任务的 action type 和 action
  6. 在 todos 的 reducer 中,完成添加任务的状态更新
  7. 对添加任务功能进行非空校验和清空文本框的操作

落地代码:

TodoHeader.js 中:

  1. import { useState } from 'react'
  2. import { useDispatch } from 'react-redux'
  3. import { addTodo } from '../store/actions/todos'
  4. export const TodoHeader = () => {
  5. // 创建 dispatch
  6. const dispatch = useDispatch()
  7. // 用户输入的任务
  8. const [text, setText] = useState('')
  9. // 点击回车键触发的事件
  10. const onAddTodo = e => {
  11. if (e.keyCode !== 13) return
  12. if (text.trim() === '') return
  13. // 分发添加任务的 action
  14. dispatch(addTodo(text))
  15. // 清空输入框的值
  16. setText('')
  17. }
  18. return (
  19. <header className="header">
  20. <h1>todos</h1>
  21. <input
  22. className="new-todo"
  23. placeholder="What needs to be done?"
  24. autoFocus
  25. value={text}
  26. onChange={e => setText(e.target.value)}
  27. onKeyDown={onAddTodo}
  28. />
  29. </header>
  30. )
  31. }

actionTypes/todos.js 中:

  1. // 添加任务
  2. export const ADD_TODO = 'todos/add'

actions/todos.js 中:

  1. export const addTodo = text => ({
  2. type: types.ADD_TODO,
  3. payload: text
  4. })

reducers/todos.js 中:

  1. // 添加任务
  2. case types.ADD_TODO:
  3. // 获取所有任务的 ID
  4. let ids = state.map(item => {
  5. return item.id
  6. })
  7. // 计算所有任务 ID 的最大值,然后 + 1
  8. let maxId = Math.max.apply(null, ids) + 1
  9. return [
  10. ...state,
  11. {
  12. id: maxId,
  13. text: action.payload,
  14. done: false
  15. }
  16. ]

10.9 全选和反选

目标:能够实现全选功能

image.png

功能分析:

此处的全选功能,类似于前面 购物车案例 中的全选功能。购物车案例是通过 添加一个新的状态(checkAll)来实现全选功能。但是,此处我们来进行一些优化:

  1. 问题:实现一个功能的时候,如何判断要不要添加一个新的状态?

  2. 回答:看该功能能不能直接通过现有的状态来实现,如果能就直接根据现有状态派生出一个数据,通过该数据来完成功能即可;否则,就得添加新状态了,比如,处理的全选按钮的选中状态可以直接从 todos 任务列表数据中得到:

    1. // 根据 todos 数据来得到全选按钮是否选中:
    2. const checkAll = todos.every(item => item.done)

实现步骤:

  1. 在 TodoMain 组件中,根据任务列表数据得到全选按钮是否选中的状态数据 checkAll
  2. 将 checkAll 设置为全选复选框的 checked 属性值
  3. 为复选框绑定 change 事件,在事件处理程序中分发全选的 action
  4. 分别添加全选的 action type 和 action
  5. 在 todos 的 reducer 中,根据全选按钮的选中状态切换每个任务项的选中状态

落地代码:

TodoMain.js 中:

  1. import { TodoItem } from './TodoItem'
  2. import { useSelector } from 'react-redux'
  3. + import { useDispatch } from 'react-redux'
  4. + import { toggleAll } from '../store/actions/todos'
  5. export const TodoMain = () => {
  6. // 获取状态
  7. const list = useSelector(state => state.todos)
  8. + // 全选与否的状态
  9. + const checkAll = list.every(item => item.done)
  10. + // 创建 dispatch
  11. + const dispatch = useDispatch()
  12. return (
  13. <section className="main">
  14. <input
  15. id="toggle-all"
  16. className="toggle-all"
  17. type="checkbox"
  18. + checked={checkAll}
  19. + onChange={e => dispatch(toggleAll(e.target.checked))}
  20. />
  21. <label htmlFor="toggle-all">Mark all as complete</label>
  22. <ul className="todo-list">
  23. {
  24. list.map(item => (
  25. <TodoItem key={item.id} {...item} />
  26. ))
  27. }
  28. </ul>
  29. </section>
  30. )
  31. }

actionTypes/todos.js 中:

  1. // 全选
  2. export const TOGGLE_ALL = 'todos/toggleAll'

actions/todos.js 中:

  1. export const toggleAll = checked => ({
  2. type: types.TOGGLE_ALL,
  3. payload: checked
  4. })

reducers/todos.js 中:

  1. // 全选全不选
  2. case types.TOGGLE_ALL:
  3. return state.map(item => ({ ...item, done: action.payload }))

10.10 清空已完成任务

目标:能够实现清空已完成的任务

image.png

实现步骤:

  1. 在 TodoFooter 组件中,给清除已完成任务的按钮绑定点击事件
  2. 在点击事件中分发清空已完成任务的 action
  3. 在 actionTypes/todos.js 文件中,创建并导出清空已完成任务的 action type
  4. 在 actions/todos.js 文件中,创建清空已完成任务的 action
  5. 在 reducers/todos.js 中,根据 action 类型清空已完成任务

落地代码:

TodoFooter.js 中:

  1. + import { useSelector, useDispatch } from 'react-redux'
  2. + import { clearDone } from '../store/actions/todos'
  3. export const TodoFooter = () => {
  4. // 获取到未完成任务数量
  5. const leftCount = useSelector(state => state.todos.filter(item => !item.done).length)
  6. + // 创建 dispatch
  7. + const dispatch = useDispatch()
  8. return (
  9. <footer className="footer">
  10. <span className="todo-count">
  11. <strong>{leftCount}</strong> item left
  12. </span>
  13. <ul className="filters">
  14. {/* ...... */}
  15. </ul>
  16. + <button className="clear-completed" onClick={() => dispatch(clearDone())}>Clear completed</button>
  17. </footer>
  18. )
  19. }

actionTypes/todos.js 中:

  1. // 清空已完成
  2. export const CLEAR_DONE = 'todos/clearDone'

actions/todos.js 中:

  1. // 清空已完成任务
  2. export const clearDone = () => ({
  3. type: types.CLEAR_DONE
  4. })

reducers/todos.js 中:

  1. // 清空已完成
  2. case types.CLEAR_DONE:
  3. return state.filter(item => !item.done)

10.11-修改任务-展示修改文本框

目标:能够实现双击修改任务功能

image.png

案例分析:

效果:双击哪个任务名称,就展示哪个任务的修改
因为需要控制文本框的展示或隐藏,所以,需要添加一个新的状态值来控制
并且,每个任务项都可以编辑,所以,需要为 TodoItem 组件添加该状态,即:每个任务项控制自己的编辑状态

实现步骤:

  1. 为 TodoItem 组件添加状态 showEdit 值为 布尔值,用来表示是否展示编辑状态
  2. 为任务项添加双击事件,将 showEdit 值设置为 true
  3. 根据 showEdit 的值为任务项添加 editing 类名

落地代码:

TodoItem.js 中:

  1. + import { useState } from 'react'
  2. import classNames from 'classnames'
  3. // 导入 useDispatch 分发 action
  4. import { useDispatch } from 'react-redux'
  5. // 导入 action-creator
  6. import { delTodo, toggleTodo } from '../store/actions/todos'
  7. export const TodoItem = ({ id, text, done }) => {
  8. + const dispatch = useDispatch()
  9. const [showEdit, setShowEdit] = useState(false)
  10. return (
  11. // 编辑时,添加类名:editing
  12. <li className={classNames({ completed: done, editing: showEdit })}>
  13. <div className="view">
  14. <input className="toggle" type="checkbox" checked={done} onChange={() => dispatch(toggleTodo(id))} />
  15. + <label onDoubleClick={() => setShowEdit(true)}>{text}</label>
  16. <button className="destroy" onClick={() => dispatch(delTodo(id))}></button>
  17. </div>
  18. <input className="edit" />
  19. </li>
  20. )
  21. }

10.12-修改任务-自动获取焦点

目标:能够在展示编辑状态时让文本框自动获得焦点

image.png

案例分析:

文本框获得焦点是一个 DOM 操作,可以通过 useRef hook 来拿到文本框的 DOM 对象

要想让文本框获得焦点,必须要在文本框展示后来操作。而文本框的展示是通过一个状态 showEdit 来控制的,也就是必须在状态更新后来操作。
问题:使用 hooks 如何在某个状态更新后,来执行相应的逻辑?
回答:使用 useEffect hook 来监听 showEdit 状态的改变

实现步骤:

  1. 在 TodoItem 组件中,使用 useRef hook 创建 ref 对象
  2. 将 ref 设置为编辑任务文本框的 ref 属性
  3. 监听 showEdit 状态的改变
  4. 判断 showEdit 是否为 true,也就是是否为编辑状态
  5. 如果是,就让文本框获得焦点

落地代码:

TodoItem.js 中:

  1. + import { useState, useEffect, useRef } from 'react'
  2. import classNames from 'classnames'
  3. // 导入 useDispatch 分发 action
  4. import { useDispatch } from 'react-redux'
  5. // 导入 action-creator
  6. import { delTodo, toggleTodo } from '../store/actions/todos'
  7. export const TodoItem = ({ id, text, done }) => {
  8. // 创建 dispatch
  9. const dispatch = useDispatch()
  10. // 新增状态控制编辑框的隐藏和展示
  11. const [showEdit, setShowEdit] = useState(false)
  12. + // 创建 ref
  13. + const inputRef = useRef(null)
  14. + // 监听
  15. + useEffect(() => {
  16. + if (showEdit) {
  17. + inputRef.current.focus()
  18. + }
  19. + }, [showEdit])
  20. return (
  21. // 编辑时,添加类名:editing
  22. <li className={classNames({ completed: done, editing: showEdit })}>
  23. {/* .... */}
  24. + <input className="edit" ref={inputRef} />
  25. </li>
  26. )
  27. }

10.13-修改任务-失焦点隐藏编辑状态

目标:能够实现编辑文本框失去焦点时隐藏编辑状态

实现步骤:

  1. 为编辑文本框绑定失焦点事件
  2. 在失焦点事件中,将 showEdit 状态设置为 false

落地代码:

TodoItem.js 中:

  1. // ...
  2. export const TodoItem = ({ id, text, done }) => {
  3. // ....
  4. const onBlur = () => {
  5. setShowEdit(false)
  6. }
  7. return (
  8. // 编辑时,添加类名:editing
  9. <li className={classNames({ completed: done, editing: showEdit })}>
  10. {/* .... */}
  11. <input className="edit" ref={inputRef} onBlur={onBlur} />
  12. </li>
  13. )
  14. }

10.14-修改任务-编辑文本框展示任务名称

目标:能够在编辑时的文本框中展示任务名称

image.png

实现步骤:

  1. 添加状态用来控制文本框的值
  2. 状态的默认值为:props.text
  3. 为编辑文本框添加 change 事件来修改对应状态的值

落地代码:

TodoItem.js 中:

  1. export const TodoItem = ({ id, text, done }) => {
  2. // ....
  3. + // 保存当前编辑项的任务名称
  4. + const [todoName, setTodoName] = useState(text)
  5. return (
  6. // 编辑时,添加类名:editing
  7. <li className={classNames({ completed: done, editing: showEdit })}>
  8. {/* .... */}
  9. <input
  10. className="edit"
  11. ref={inputRef}
  12. onBlur={onBlur}
  13. + value={todoName}
  14. + onChange={e => setTodoName(e.target.value)}
  15. />
  16. </li>
  17. )
  18. }

10.15-任务修改-编辑功能完成

目标:能够实现敲回车时保存任务名称

实现步骤:

  1. 为编辑文本框绑定 keyDown 事件
  2. 在 keyDown 事件中判断是否按下回车键,如果不是直接 return 不执行任何操作
  3. 判断编辑时任务名称是否为空,如果是直接 return 不执行任何操作
  4. 分发修改任务名称的 action,并将 showEdit 设置为 false 来隐藏编辑状态
  5. 在 actionTypes/todos.js 文件中,创建并导出修改任务名称的 action type
  6. 在 actions/todos.js 文件中,创建修改任务名称任务的 action
  7. 在 reducers/todos.js 中,根据 action 类型修改任务名称
  8. 同时处理失焦点时保存任务名称

落地代码:

TodoItem.js 中:

  1. // ...
  2. export const TodoItem = ({ id, text, done }) => {
  3. // ...
  4. // 失去焦点时
  5. const onBlur = () => {
  6. if (todoName.trim() === '') return
  7. dispatch(updateTodo(id, todoName))
  8. setShowEdit(false)
  9. }
  10. // 编辑完回车
  11. const onUpdateTodo = e => {
  12. if (e.keyCode !== 13) return
  13. if (todoName.trim() === '') return
  14. dispatch(updateTodo(id, todoName))
  15. setShowEdit(false)
  16. }
  17. return (
  18. // 编辑时,添加类名:editing
  19. <li className={classNames({ completed: done, editing: showEdit })}>
  20. {/* .... */}
  21. <input
  22. className="edit"
  23. ref={inputRef}
  24. onBlur={onBlur}
  25. value={todoName}
  26. onChange={e => setTodoName(e.target.value)}
  27. onKeyDown={onUpdateTodo}
  28. />
  29. </li>
  30. )
  31. }

actionTypes/todos.js 中:

  1. // 修改任务名称
  2. export const UPDATE_TODO = 'todos/updateTodo'

actions/todos.js 中:

  1. // 更新任务
  2. export const updateTodo = (id, text) => ({
  3. type: types.UPDATE_TODO,
  4. payload: {
  5. id,
  6. text
  7. }
  8. })

reducers/todos.js 中:

  1. // 修改任务名称
  2. case types.UPDATE_TODO:
  3. return state.map(item => {
  4. if (item.id === action.payload.id) {
  5. return {
  6. ...item,
  7. text: action.payload.text
  8. }
  9. }
  10. return item
  11. })

10.16-切换状态-准备 filter 状态

目标:能够展示不同状态的任务列表

image.png

功能分析:

任务列表的展示有 3 种情况:1 展示所有任务 2 展示已完成任务 3 展示未完成任务

  1. 操作方式:点击底部 All、Active、Completed 按钮时,展示对应状态的任务列表,因为切换展示不同状态时,页面中的内容会发生改变,而我们知道:只有状态更新后,页面才会改变(重新渲染)

  2. 问题:能不能直接修改 redux 中存储的任务列表状态?比如,点击 Active 时,将任务列表数据修改为只包含未完成的任务列表数据

  3. 回答:不能,因为这样操作后,再想展示已完成任务列表数据就无法展示了(因为数据中已经没有已完成的数据了)

因为不能直接修改任务列表状态,所以,就必须要添加一个新的状态,来实现展示不同状态的任务列表
所以,在 Redux 中添加一个新的状态:filter 表示当前要展示什么状态的任务列表

  1. filter 的值可以是:'all' | 'active' | 'completed'

实现步骤:

  1. 创建 reducers/filter.js 文件,用来处理展示不同状态的任务列表
  2. 为 filter reducer 指定默认值为:’all’ 表示默认展示所有任务列表数据
  3. 将 filter reducer 合并到根 redcuer
  4. 在 TodoFooter 组件中获取到 filter 状态
  5. 根据 filter 状态来控制底部三个按钮的选中

落地代码:

reducers/filter.js 中:

  1. const filter = (state = 'all', action) => {
  2. return state
  3. }
  4. export default filter

reducers/index.js 中:

  1. import { combineReducers } from 'redux'
  2. // 导入 reducer
  3. import todos from './todos'
  4. import filter from './filter'
  5. // 合并 reducer
  6. const rootReducer = combineReducers({
  7. todos,
  8. filter
  9. })
  10. export default rootReducer

TodoFooter.js 中:

  1. import { useSelector, useDispatch } from 'react-redux'
  2. import { clearDone } from '../store/actions/todos'
  3. + import classNames from 'classnames'
  4. export const TodoFooter = () => {
  5. // 获取到未完成任务数量
  6. const leftCount = useSelector(state => state.todos.filter(item => !item.done).length)
  7. + // 获取筛选的状态
  8. + const filter = useSelector(state => state.filter)
  9. // 创建 dispatch
  10. const dispatch = useDispatch()
  11. return (
  12. <footer className="footer">
  13. <span className="todo-count">
  14. <strong>{leftCount}</strong> item left
  15. </span>
  16. <ul className="filters">
  17. <li>
  18. + <a className={classNames({ selected: filter === 'all' })} href="#/">All</a>
  19. </li>
  20. <li>
  21. + <a className={classNames({ selected: filter === 'active' })} href="#/">Active</a>
  22. </li>
  23. <li>
  24. + <a className={classNames({ selected: filter === 'completed' })} href="#/">Completed</a>
  25. </li>
  26. </ul>
  27. <button className="clear-completed" onClick={() => dispatch(clearDone())}>Clear completed</button>
  28. </footer>
  29. )
  30. }

10.17-切换状态-切换filter状态

目标:能够在点击底部按钮时切换filter的高亮状态

实现步骤:

  1. 在 TodoFooter 组件中,给 3 个按钮绑定点击事件
  2. 在点击事件分发切换 filter 状态的 action,并把自己当前的状态值传递给 action
  3. 创建 actionTypes/filter.js 文件,创建切换filter状态的 action type 并导出
  4. 创建 actions/filter.js 文件,创建切换filter状态的 action 并导出
  5. 在 filter 的 reducer 中,处理切换 filter 状态的 action

落地代码:

TodoFooter.js 中:

  1. import { changeFilter } from '../store/actions/filter'
  2. export const TodoFooter = () => {
  3. return (
  4. // ...
  5. <ul className="filters">
  6. <li>
  7. <a
  8. onClick={() => dispatch(changeFilter('all'))}
  9. href="#/"
  10. >
  11. All
  12. </a>
  13. </li>
  14. <li>
  15. <a
  16. onClick={() => dispatch(changeFilter('active'))}
  17. href="#/"
  18. >
  19. Active
  20. </a>
  21. </li>
  22. <li>
  23. <a
  24. onClick={() => dispatch(changeFilter('completed'))}
  25. href="#/"
  26. >
  27. Completed
  28. </a>
  29. </li>
  30. </ul>
  31. )
  32. }

actionTypes/filter.js 中:

  1. // 切换 filter 状态
  2. export const CHANGE_FILTER = 'filter/changeFilter'

actions/filter.js 中:

  1. import * as types from '../actionTypes/filter'
  2. export const changeFilter = filter => ({
  3. type: types.CHANGE_FILTER,
  4. payload: filter
  5. })

reducers/filter.js 中:

  1. import * as types from '../actionTypes/filter'
  2. const filter = (state = 'all', action) => {
  3. switch (action.type) {
  4. case types.CHANGE_FILTER:
  5. return action.payload
  6. default:
  7. return state
  8. }
  9. }
  10. export default filter

10.18-切换状态-筛选状态

目标:能够从任务列表数据中筛选出对应状态的数据

实现步骤:

  1. 在 TodoMain 组件中,根据 filter 状态来筛选数据

落地代码:

  1. // 获取状态
  2. // const list = useSelector(state => state.todos)
  3. const list = useSelector(state => {
  4. // 根据任务状态,筛选出对应的任务列表数据
  5. if (state.filter === 'active') {
  6. return state.todos.filter(item => !item.done)
  7. } else if (state.filter === 'completed') {
  8. return state.todos.filter(item => item.done)
  9. } else {
  10. return state.todos
  11. }
  12. })