在这一部分中,咱们将趁热打铁,运用上篇教程学到的 Redux 三大核心概念来将待办事项的剩下部分重构完成,它涉及到将 TodoList 和 Footer 部分的相关代码重构到 Redux,并使用 Redux combineReducers API 进行逻辑拆分和组合,使得咱们能够在使用 Redux 便利的同时,又不至于让应用的逻辑看起来臃肿不堪,复用 React 组件化的便利,咱们可让状态的处理也 “组件化”。前端
欢迎阅读 Redux 包教包会系列:react
此教程属于 React 前端工程师学习路线的一部分,点击可查看所有内容。
在以前的几个小节中,咱们已经把 Redux 的核心概念讲完了,而且运用这些概念重构了一部分待办事项应用,在这一小节中,咱们将趁热打铁,完整地运用以前学到的知识,继续用 Redux 重构咱们的应用。git
此时若是你在浏览器里面尝试这个待办事项小应用,你会发现它还只能够添加新的待办事项,对于 “完成和重作待办事项” 以及 “过滤查看待办事项” 这两个功能,目前咱们尚未使用 Redux 实现。因此当你点击单个待办事项时,浏览器会报错;当你点击底部的三个过滤器按钮时,浏览器不会有任何反应。github
在这一小节中,咱们将使用 Redux 重构 “完成和重作待办事项” 功能,即你能够经过点击某个待办事项来完成它。算法
咱们将运用 Redux 最佳实践的开发方式来重构这一功能:编程
connect
组件以及在组件中 dispatch
Action之后在开发 Redux 应用的时候,均可以使用这三步流程来周而复始地开发新的功能,或改进现有的功能。redux
首先咱们要定义 “完成待办事项” 这一功能所涉及的 Action,打开 src/actions/index.js
,修改内容内容以下:数组
let nextTodoId = 0; export const addTodo = text => ({ type: "ADD_TODO", id: nextTodoId++, text }); export const toggleTodo = id => ({ type: "TOGGLE_TODO", id });
能够看到,咱们定义并导出了一个 toggleTodo
箭头函数,它接收 id
并返回一个类型为 "TOGGLE_TODO"
的 Action。浏览器
接着咱们来定义响应 dispatch(action)
的 Reducers,打开 src/index.js
,修改 rootReducer
函数以下:前端工程师
import React from "react"; import ReactDOM from "react-dom"; import App, { VisibilityFilters } from "./components/App"; import { createStore } from "redux"; import { Provider } from "react-redux"; const initialState = { todos: [ { id: 1, text: "你好, 图雀", completed: false }, { id: 2, text: "我是一只小小小小图雀", completed: false }, { id: 3, text: "小若燕雀,亦可一展宏图!", completed: false } ], filter: VisibilityFilters.SHOW_ALL }; const rootReducer = (state, action) => { switch (action.type) { case "ADD_TODO": { const { todos } = state; return { ...state, todos: [ ...todos, { id: action.id, text: action.text, completed: false } ] }; } case "TOGGLE_TODO": { const { todos } = state; return { ...state, todos: todos.map(todo => todo.id === action.id ? { ...todo, completed: !todo.completed } : todo ) }; } default: return state; } }; const store = createStore(rootReducer, initialState); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );
能够看到,咱们在 switch
语句里面添加了一个 "TOGGLE_TODO"
的判断,并根据 action.id
来判断对应操做的 todo,取反它目前的 completed
属性,用来表示从完成到未完成,或从未完成到完成的操做。
当定义了 Action,声明了响应 Action 的 Reducers 以后,咱们开始定义 React 和 Redux 交流的接口:connect
和 dispatch
,前者负责将 Redux Store 的内容整合进 React,后者负责从 React 中发出操做 Redux Store 的指令。
咱们打开 src/components/TodoList.js
文件,对文件内容做出以下的修改:
import React from "react"; import PropTypes from "prop-types"; import Todo from "./Todo"; import { connect } from "react-redux"; import { toggleTodo } from "../actions"; const TodoList = ({ todos, dispatch }) => ( <ul> {todos.map(todo => ( <Todo key={todo.id} {...todo} onClick={() => dispatch(toggleTodo(todo.id))} /> ))} </ul> ); TodoList.propTypes = { todos: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired }).isRequired ).isRequired }; export default connect()(TodoList);
能够看到,咱们对文件作出了如下几步修改:
react-redux
中导出 connect
函数,它负责给 TodoList
传入 dispatch
函数,使得咱们能够在 TodoList
组件中 dispatch
Action。toggleTodo
Action Creators,并将以前从父组件接收 toggleTodo
方法并调用的方式改为了当 Todo 被点击以后,咱们 dispatch(toggle(todo.id))
。propsTypes
中再也不须要的 toggleTodo
。当咱们经过以上三步整合了 Redux 的内容以后,咱们就能够删除原 App.js
中没必要要的代码了,打开 src/components/App.js
修改内容以下:
import React from "react"; import AddTodo from "./AddTodo"; import TodoList from "./TodoList"; import Footer from "./Footer"; import { connect } from "react-redux"; export const VisibilityFilters = { SHOW_ALL: "SHOW_ALL", SHOW_COMPLETED: "SHOW_COMPLETED", SHOW_ACTIVE: "SHOW_ACTIVE" }; const getVisibleTodos = (todos, filter) => { switch (filter) { case VisibilityFilters.SHOW_ALL: return todos; case VisibilityFilters.SHOW_COMPLETED: return todos.filter(t => t.completed); case VisibilityFilters.SHOW_ACTIVE: return todos.filter(t => !t.completed); default: throw new Error("Unknown filter: " + filter); } }; class App extends React.Component { constructor(props) { super(props); this.setVisibilityFilter = this.setVisibilityFilter.bind(this); } setVisibilityFilter(filter) { this.setState({ filter: filter }); } render() { const { todos, filter } = this.props; return ( <div> <AddTodo /> <TodoList todos={getVisibleTodos(todos, filter)} /> <Footer filter={filter} setVisibilityFilter={this.setVisibilityFilter} /> </div> ); } } const mapStateToProps = (state, props) => ({ todos: state.todos, filter: state.filter }); export default connect(mapStateToProps)(App);
能够看到,咱们删除了 toggleTodo
方法,并对应删除了定义在 constructor
中的 toggleTodo
定义以及在 render
方法中,传给 TodoList
的 toggleTodo
属性。
保存上述修改的代码,打开浏览器,你应该又能够点击单个待办事项来完成和重作它了:
在本节中,咱们介绍了开发 Redux 应用的最佳实践,并经过重构 "完成和重作待办事项“ 这一功能来详细实践了这一最佳实践。
这一节中,咱们将继续重构剩下的部分。咱们将继续遵循上一节提到的 Redux 开发的最佳实践:
connect
组件以及在组件中 dispatch
Action打开 src/actions/index.js
文件,修改内容以下:
let nextTodoId = 0; export const addTodo = text => ({ type: "ADD_TODO", id: nextTodoId++, text }); export const toggleTodo = id => ({ type: "TOGGLE_TODO", id }); export const setVisibilityFilter = filter => ({ type: "SET_VISIBILITY_FILTER", filter });
能够看到咱们建立了一个名为 setVisibilityFilter
的 Action Creators,它接收 filter
参数,而后返回一个类型为 "SET_VISIBILITY_FILTER"
的 Action。
打开 src/index.js
文件,修改代码以下:
import React from "react"; import ReactDOM from "react-dom"; import App, { VisibilityFilters } from "./components/App"; import { createStore } from "redux"; import { Provider } from "react-redux"; const initialState = { todos: [ { id: 1, text: "你好, 图雀", completed: false }, { id: 2, text: "我是一只小小小小图雀", completed: false }, { id: 3, text: "小若燕雀,亦可一展宏图!", completed: false } ], filter: VisibilityFilters.SHOW_ALL }; const rootReducer = (state, action) => { switch (action.type) { case "ADD_TODO": { const { todos } = state; return { ...state, todos: [ ...todos, { id: action.id, text: action.text, completed: false } ] }; } case "TOGGLE_TODO": { const { todos } = state; return { ...state, todos: todos.map(todo => todo.id === action.id ? { ...todo, completed: !todo.completed } : todo ) }; } case "SET_VISIBILITY_FILTER": { return { ...state, filter: action.filter }; } default: return state; } }; const store = createStore(rootReducer, initialState); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );
能够看到,咱们增长了一条 case
语句,来响应 "SET_VISIBILITY_FILTER"
Action,经过接收新的 filter
来更新 Store 中的状态。
打开 src/components/Footer.js
文件,修改内容以下:
import React from "react"; import Link from "./Link"; import { VisibilityFilters } from "./App"; import { connect } from "react-redux"; import { setVisibilityFilter } from "../actions"; const Footer = ({ filter, dispatch }) => ( <div> <span>Show: </span> <Link active={VisibilityFilters.SHOW_ALL === filter} onClick={() => dispatch(setVisibilityFilter(VisibilityFilters.SHOW_ALL))} > All </Link> <Link active={VisibilityFilters.SHOW_ACTIVE === filter} onClick={() => dispatch(setVisibilityFilter(VisibilityFilters.SHOW_ACTIVE)) } > Active </Link> <Link active={VisibilityFilters.SHOW_COMPLETED === filter} onClick={() => dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED)) } > Completed </Link> </div> ); export default connect()(Footer);
能够看到,上面的文件主要作了这几件事:
react-redux
中导出 connect
函数,它负责给 Footer
传入 dispatch
函数,使得咱们能够在 Footer
组件中 dispatch
Action。setVisibilityFilter
Action Creators,并将以前从父组件接收 setVisibilityFilter
方法并调用的方式改为了当 Link 被点击以后,咱们 dispatch
对应的 Action 。当咱们经过以上三步整合了 Redux 的内容以后,咱们就能够删除原 App.js
中没必要要的代码了,打开 src/components/App.js
修改内容以下:
import React from "react"; import AddTodo from "./AddTodo"; import TodoList from "./TodoList"; import Footer from "./Footer"; import { connect } from "react-redux"; export const VisibilityFilters = { SHOW_ALL: "SHOW_ALL", SHOW_COMPLETED: "SHOW_COMPLETED", SHOW_ACTIVE: "SHOW_ACTIVE" }; const getVisibleTodos = (todos, filter) => { switch (filter) { case VisibilityFilters.SHOW_ALL: return todos; case VisibilityFilters.SHOW_COMPLETED: return todos.filter(t => t.completed); case VisibilityFilters.SHOW_ACTIVE: return todos.filter(t => !t.completed); default: throw new Error("Unknown filter: " + filter); } }; class App extends React.Component { render() { const { todos, filter } = this.props; return ( <div> <AddTodo /> <TodoList todos={getVisibleTodos(todos, filter)} /> <Footer filter={filter} /> </div> ); } } const mapStateToProps = (state, props) => ({ todos: state.todos, filter: state.filter }); export default connect(mapStateToProps)(App);
能够看到,咱们删除了 setVisibilityFilter
方法,并对应删除了定义在 constructor
中的 setVisibilityFilter
定义以及在 render
方法中,传给 Footer
的 setVisibilityFilter
属性。
由于 constructor
方法中已经不须要再定义内容了,因此咱们删掉了它。
保存上述修改的代码,打开浏览器,你应该又能够继续点击底部的按钮来过滤完成和未完成的待办事项了:
在本节中,咱们介绍了开发 Redux 应用的最佳实践,并经过重构 "过滤查看待办事项“ 这一功能来详细实践了这一最佳实践。
自此,咱们已经使用 Redux 重构了整个待办事项小应用,可是重构完的这份代码还显得有点乱,不一样类型的组件状态混在一块儿。当咱们的应用逐渐变得复杂时,咱们的 rootReducer
就会变得很是冗长,因此是时候考虑拆分不一样组件的状态了。
咱们将在下一节中讲解如何将不一样组件的状态进行拆分,以确保咱们在编写大型应用时也能够显得很从容。
当应用逻辑逐渐复杂的时候,咱们就要考虑将巨大的 Reducer 函数拆分红一个个独立的单元,这在算法中被称为 ”分而治之“。
Reducers 在 Redux 中其实是用来处理 Store 中存储的 State 中的某个部分,一个 Reducer 和 State 对象树中的某个属性一一对应,一个 Reducer 负责处理 State 中对应的那个属性。好比咱们来看一下如今咱们的 State 的结构:
const initialState = { todos: [ { id: 1, text: "你好, 图雀", completed: false }, { id: 2, text: "我是一只小小小小图雀", completed: false }, { id: 3, text: "小若燕雀,亦可一展宏图!", completed: false } ], filter: VisibilityFilters.SHOW_ALL };
由于 Reducer 对应着 State 相关的部分,这里咱们的 State 有两个部分:todos
和 filter
,因此咱们能够编写两个对应的 Reducer。
在 Redux 最佳实践中,由于 Reducer 对应修改 State 中的相关部分,当 State 对象树很大时,咱们的 Reducer 也会有不少,因此咱们通常会单独建一个 reducers
文件夹来存放这些 "reducers“。
咱们在 src
目录下新建 reducers
文件夹,而后在里面新建一个 todos.js
文件,表示处理 State 中对应 todos
属性的 Reducer:
const initialTodoState = [ { id: 1, text: "你好, 图雀", completed: false }, { id: 2, text: "我是一只小小小小图雀", completed: false }, { id: 3, text: "小若燕雀,亦可一展宏图!", completed: false } ]; const todos = (state = initialTodoState, action) => { switch (action.type) { case "ADD_TODO": { return [ ...state, { id: action.id, text: action.text, completed: false } ]; } case "TOGGLE_TODO": { return state.map(todo => todo.id === action.id ? { ...todo, completed: !todo.completed } : todo ); } default: return state; } }; export default todos;
能够看到,上面的代码作了这几件事:
initialState
里面的 todos
部分拆分到了 src/reducers/todos.js
文件里,咱们定义了一个 initialTodoState
表明以前的 initialState
的 todos
部分,它是一个数组,并把它赋值给 todos
函数中 state
参数的默认值,即当调用此函数时,若是传进来的 state 参数为 undefined
或者 null
时,这个 state
就是 initialState
。todos
箭头函数,它的结构和 rootReducer
相似,都是接收两个参数:state
和 action
,而后进入一个 switch
判断语句,根据 action.type
判断要相应的 Action 类型,而后对 state
执行对应的操做。注意咱们的
todos
reducers 只负责处理原initialState
的todos
部分,因此这里它的state
就是原todos
属性,它是一个数组,因此咱们在switch
语句里,进行数据改变时,要对数组进行操做,并最后返回一个新的数组。
咱们前面使用 todos
reducer 解决了原 initialState
的 todos
属性操做问题,如今咱们立刻来说解剩下的 filter
属性的操做问题。
在 src/reducers
文件夹下建立 filter.js
文件,在其中加入以下的内容:
import { VisibilityFilters } from "../components/App"; const filter = (state = VisibilityFilters.SHOW_ALL, action) => { switch (action.type) { case "SET_VISIBILITY_FILTER": return action.filter; default: return state; } }; export default filter;
能够看到咱们定义了一个 filter
箭头函数,它接收两个参数:state
和 action
,由于这个 filter
reducer 只负责处理原 initialState
的 filter
属性部分,因此这里这个 state
参数就是原 filter
属性,这里咱们给了它一个默认值。
注意filter 函数的剩余部分和
rootReducer
相似,可是注意这里它的state
是对filter
属性进行操做,因此当判断"SET_VISIBILITY_FILTER"
action 类型时,它只是单纯的返回action.filter
。
当咱们将 rootReducer
的逻辑拆分,并对应处理 Store 中保存的 State 中的属性以后,咱们能够确保每一个 reducer 都很小,这个时候咱们就要考虑如何将这些小的 reducer 组合起来,构成最终的 rootReducer,这种组合就像咱们组合 React 组件同样,最终只有一个根级组件,在咱们的待办事项小应用里面,这个组件就是 App.js
组件。
Redux 为咱们提供了 combineReducers
API,用来组合多个小的 reducer,咱们在 src/reducers
文件夹下建立 index.js
文件,并在里面添加以下内容:
import { combineReducers } from "redux"; import todos from "./todos"; import filter from "./filter"; export default combineReducers({ todos, filter });
能够看到,咱们从 redux
模块中导出了 combineReducers
函数,而后导出了以前定义的 todos
和 filter
reducer。
接着咱们经过对象简洁表示法,将 todos
和 filter
做为对象属性合在一块儿,而后传递给 combineReducers
函数,这里 combineReducers
内部就会对 todos
和 filter
进行操做,而后生成相似咱们以前的 rootReducer
形式。最后咱们导出生成的 rootReducer
。
combineReducers 主要有两个做用:
1)组合全部 reducer 的 state,最后组合成相似咱们以前定义的
initialState
对象状态树。即这里
todos
reducer 的 state 为:state = [ { id: 1, text: "你好, 图雀", completed: false }, { id: 2, text: "我是一只小小小小图雀", completed: false }, { id: 3, text: "小若燕雀,亦可一展宏图!", completed: false } ];
filter
reducer 的 state 为:state = VisibilityFilters.SHOW_ALL那么经过
combineReducers
组合这两个reducer
的state
获得的最终结果为:state = { todos: [ { id: 1, text: "你好, 图雀", completed: false }, { id: 2, text: "我是一只小小小小图雀", completed: false }, { id: 3, text: "小若燕雀,亦可一展宏图!", completed: false } ], filter: VisibilityFilters.SHOW_ALL };这个经过
combineReducers
组合后的最终state
就是存储在 Store 里面的那棵 State JavaScript 对象状态树。2)分发
dispatch
的 Action。经过
combineReducers
组合todos
和filter
reducer 以后,从 React 组件中dispatch
Action会遍历检查todos
和filter
reducer,判断是否存在响应对应action.type
的case
语句,若是存在,全部的这些case
语句都会响应。
当咱们将原 rootReducer
拆分红了 todos
和 filter
两个 reducer ,并经过 redux
提供的 combineReducers
API 进行组合后,咱们以前在 src/index.js
定义的 initialState
和 rootReducer
就再也不须要了,因此咱们立刻来删除它们:
import React from "react"; import ReactDOM from "react-dom"; import App, { VisibilityFilters } from "./components/App"; import { createStore } from "redux"; import { Provider } from "react-redux"; import rootReducer from "./reducers"; const store = createStore(rootReducer); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );
能够看到,咱们从删除了以前在 src/index.js
定义的 rootReducer
,转而使用了从 src/reducers/index.js
导出的 rootReducer
。
而且咱们咱们以前讲到,combineReducers
的第一个功能就是组合多个 reducer 的 state,最终合并成一个大的 JavaScript 对象状态树,而后自动存储在 Redux Store 里面,因此咱们再也不须要给 createStore
显式的传递第二个 initialState
参数了。
保存修改的内容,打开浏览器,能够照样能够操做全部的功能,你能够加点待办事项,点击某个待办事项以完成它,经过底部的三个过滤按钮查看不一样状态下的待办事项:
在这一小节中,咱们讲解了 redux
提供的 combineReducers
API,它主要解决两个问题:
当有了 combineReducers
以后,无论咱们的应用如何复杂,咱们均可以将处理应用状态的逻辑拆分都一个一个很简洁、易懂的小文件,而后组合这些小文件来完成复杂的应用逻辑,这和 React 组件的组合思想相似,能够想见,组件式编程的威力是多么巨大!
此教程属于 React 前端工程师学习路线的一部分,点击可查看所有内容。想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。