在这一部分中,咱们将趁热打铁,运用上篇教程学到的 Redux 三大核心概念来将待办事项的剩下部分重构完成,它涉及到将 TodoList 和 Footer 部分的相关代码重构到 Redux,并使用 Redux combineReducers
API 进行逻辑拆分和组合,使得咱们能够在使用 Redux 便利的同时,又不至于让应用的逻辑看起来臃肿不堪,复用 React 组件化的便利,咱们可让状态的处理也 “组件化”。最后,咱们将让 React 回归初心——专一于展示用户界面,经过“容器组件”和“展现组件”将逻辑和状态进一步分离。前端
欢迎阅读 Redux 包教包会系列:node
此教程属于React 前端工程师学习路线的一部分,欢迎来 Star 一波,鼓励咱们继续创做出更好的教程,持续更新中~。react
在上一篇教程中,咱们已经把 Redux 的核心概念讲完了,而且运用这些概念重构了一部分待办事项应用,在这一小节中,咱们将完整地运用以前学到的知识,继续用 Redux 重构咱们的应用。若是你没有读过上篇教程,想直接从这一步开始,那么请运行如下命令:git
git clone https://github.com/pftom/redux-quickstart-tutorial.git
cd redux-quickstart-tutorial
git checkout second-part
npm install && npm start
复制代码
此时若是你在浏览器里面尝试查看这个待办事项小应用,你会发现它还只能够添加新的待办事项,对于 “完成和重作待办事项” 以及 “过滤查看待办事项” 这两个功能,目前咱们尚未使用 Redux 实现。因此当你点击单个待办事项时,浏览器会报错;当你点击底部的三个过滤器按钮时,浏览器不会有任何反应。github
在这一小节中,咱们将使用 Redux 重构 “完成和重作待办事项” 功能,即你能够经过点击某个待办事项来完成它。算法
咱们将运用 Redux 最佳实践的开发方式来重构这一功能:npm
connect
组件以及在组件中 dispatch
Action之后在开发 Redux 应用的时候,均可以使用这三步流程来周而复始地开发新的功能,或改进现有的功能。编程
首先咱们要定义 “完成待办事项” 这一功能所涉及的 Action,打开 src/actions/index.js
,在最后面添加 toggleTodo
:redux
// 省略 nextTodoId 和 addTodo ...
export const toggleTodo = id => ({
type: "TOGGLE_TODO",
id
});
复制代码
能够看到,咱们定义并导出了一个 toggleTodo
箭头函数,它接收 id
并返回一个类型为 "TOGGLE_TODO"
的 Action。设计模式
接着咱们来定义响应 dispatch(action)
的 Reducers,打开 src/index.js
,修改 rootReducer
函数以下:
// ...
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;
}
};
// ...
复制代码
能够看到,咱们在 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 语句 ...
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
内容以下:
// ...
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>
);
}
}
// ...
复制代码
能够看到,咱们删除了 toggleTodo
方法,并对应删除了定义在 constructor
中的 toggleTodo
定义以及在 render
方法中,传给 TodoList
的 toggleTodo
属性。
保存上述修改的代码,打开浏览器,你应该又能够点击单个待办事项来完成和重作它了:
在本节中,咱们介绍了开发 Redux 应用的最佳实践,并经过重构 "完成和重作待办事项“ 这一功能来详细实践了这一最佳实践。
这一节中,咱们将继续重构剩下的部分。咱们将继续遵循上一节提到的 Redux 开发的最佳实践:
connect
组件以及在组件中 dispatch
Action打开 src/actions/index.js
文件,在最后面添加 setVisibilityFilter
:
// ...
export const setVisibilityFilter = filter => ({
type: "SET_VISIBILITY_FILTER",
filter
});
复制代码
能够看到咱们建立了一个名为 setVisibilityFilter
的 Action Creators,它接收 filter
参数,而后返回一个类型为 "SET_VISIBILITY_FILTER"
的 Action。
打开 src/index.js
文件,修改 rootReducer
以下:
// ...
const rootReducer = (state, action) => {
switch (action.type) {
// 省略处理 ADD_TODO 和 TOGGLE_TODO 的 reducers ...
case "SET_VISIBILITY_FILTER": {
return {
...state,
filter: action.filter
};
}
default:
return state;
}
};
// ...
复制代码
能够看到,咱们增长了一条 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
修改内容以下:
// ...
class App extends React.Component {
render() {
const { todos, filter } = this.props;
return (
<div>
<AddTodo />
<TodoList todos={getVisibleTodos(todos, filter)} />
<Footer filter={filter} />
</div>
);
}
}
// ...
复制代码
能够看到,咱们删除了 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 组件的组合思想相似,能够想见,组件式编程的威力是多么巨大!
Redux 的出现,经过将 State 从 React 组件剥离,并将其保存在 Store 里面,来确保状态来源的可预测性,你可能以为这样就已经很好了,可是 Redux 的动做还没完,它又进一步提出了展现组件(Presentational Components)和容器组件(Container Components)的概念,将纯展现性的 React 组件和状态进一步抽离。
当咱们把 Redux 状态循环图中的 View 层进一步拆分时,它看起来是这样的:
即咱们在最终渲染界面的组件和 Store 中存储的 State 之间又加了一层,咱们称这一层为它专门负责接收来自 Store 的 State,并把组件中想要发起的状态改变组装成 Action,而后经过 dispatch
函数发出。
将状态完全剥离以后剩下的那层称之为展现组件,它专门接收来自容器组件的数据,而后将其渲染成 UI 界面,并在须要改变状态时,告知容器组件,让其代为 dispatch
Action。
首先,咱们将 App.js 中的 VisibilityFilters
移到了 src/actions/index.js 的最后。由于 VisibilityFilters
定义了过滤展现 TodoList 的三种操做,和 Action 的含义更相近一点,因此咱们将类似的东西放在了一块儿。修改 src/actions/index.js 以下:
// 省略了 nextTodoId 和以前定义的三个 Action
export const VisibilityFilters = {
SHOW_ALL: "SHOW_ALL",
SHOW_COMPLETED: "SHOW_COMPLETED",
SHOW_ACTIVE: "SHOW_ACTIVE"
};
复制代码
容器组件其实也是一个 React 组件,它只是将原来从 Store 到 View 的状态和从组件中 dispatch
Action 这两个逻辑从原组件中抽离出来。
根据 Redux 的最佳实践,容器组件通常保存在 containers
文件夹中,咱们在 src
文件夹下创建一个 containers
文件夹,而后在里面新建 VisibleTodoList.js
文件,用来表示原 TodoList.js
的容器组件,并在文件中加入以下代码:
import { connect } from "react-redux";
import { toggleTodo } from "../actions";
import TodoList from "../components/TodoList";
import { VisibilityFilters } from "../actions";
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);
}
};
const mapStateToProps = state => ({
todos: getVisibleTodos(state.todos, state.filter)
});
const mapDispatchToProps = dispatch => ({
toggleTodo: id => dispatch(toggleTodo(id))
});
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
复制代码
能够看到,上面的代码主要作了这几件事情:
mapStateToProps
,这是咱们以前详细讲解过,它主要是能够获取到来自 Redux Store 的 State 以及组件自身的原 Props,而后组合这二者成新的 Props,而后传给组件,这个函数是 Store 到组件的惟一接口。这里咱们将以前定义在 App.js
中的 getVisibleTodos
函数移过来,并根据 state.filter
过滤条件返回相应须要展现的 todos
。mapDispatchToProps
函数,这个函数接收两个参数:dispatch
和 ownProps
,前者咱们很熟悉了就是用来发出更新动做的函数,后者就是原组件的 Props,它是一个可选参数,这里咱们没有声明它。咱们主要在这个函数声明式的定义全部须要 dispatch
的 Action 函数,并将其做为 Props 传给组件。这里咱们定义了一个 toggleTodo
函数,使得在组件中经过调用 toggleTodo(id)
就能够 dispatch(toggleTodo(id))
。connect
函数接收 mapStateToProps
和 mapDispatchToProps
并调用,而后再接收 TodoList 组件并调用,返回最终的导出的容器组件。当咱们编写了 TodoList
的容器组件以后,接着咱们要考虑就是抽离了 State 和 dispatch
的关于 TodoList 的展现组件了。
修改 src/components/TodoList.js
,代码以下:
import React from "react";
import PropTypes from "prop-types";
import Todo from "./Todo";
const TodoList = ({ todos, toggleTodo }) => (
<ul> {todos.map(todo => ( <Todo key={todo.id} {...todo} onClick={() => 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, toggleTodo: PropTypes.func.isRequired }; export default TodoList; 复制代码
在上面的代码中,咱们删除了 connect
和 toggleTodo
Action,并将 TodoList 接收的 dispatch
属性删除,转而改为经过 mapDispatchToProps
传进来的 toggleTodo
函数,并在 Todo 被点击时调用 toggleTodo
函数。
固然咱们的 toggleTodo
属性又回来了,因此咱们在 propTypes
中恢复以前删除的 toggleTodo
。:)
最后,咱们再也不须要 connect()(TodoList)
,由于 VisibleTodoList.js
中定义的 TodoList
的对应容器组件会取到 Redux Store 中的 State,而后传给 TodoList。
能够看到,TodoList
不用再考虑状态相关的操做,只须要专心地作好界面的展现和动做的响应。咱们进一步将状态与渲染分离,让合适的人作 TA 最擅长的事。
由于咱们将原来的 TodoList 剥离成了容器组件和 展现组件,因此咱们要将 App.js
里面对应的 TodoList
换成咱们的 VisibleTodoList
,由容器组件来提供原 TodoList 对外的接口。
咱们打开 src/components/App.js
对相应的内容做出以下修改:
import React from "react";
import AddTodo from "./AddTodo";
import VisibleTodoList from "../containers/VisibleTodoList";
import Footer from "./Footer";
import { connect } from "react-redux";
class App extends React.Component {
render() {
const { filter } = this.props;
return (
<div> <AddTodo /> <VisibleTodoList /> <Footer filter={filter} /> </div> ); } } const mapStateToProps = (state, props) => ({ filter: state.filter }); export default connect(mapStateToProps)(App); 复制代码
能够看到咱们作了这么几件事:
TodoList
更换成 VisibleTodoList。
VisibilityFilters,由于它已经被放到了
src/actions/index.js
中getVisibleTodos
,由于它已经被放到了 VisibleTodoList
中。mapStateToProps
中获取 todos
的操做,由于咱们已经在 VisibleTodoList
中获取了。App
组件中的 todos
。接着咱们处理一下因 VisibilityFilters
变更而引发的其余几个文件的导包问题。
打开 src/components/Footer.js
修改 VisibilityFilters
的导包路径:
import React from "react";
import Link from "./Link";
import { VisibilityFilters } from "../actions";
// ...
复制代码
打开 src/reducers/filter.js
修改 VisibilityFilters
的导包路径:
import { VisibilityFilters } from "../actions";
// ...
复制代码
由于咱们在 src/actions/index.js
中的 nextTodoId
是从 0 开始自增的,因此以前咱们定义的 initialTodoState
会出现一些问题,好比新添加的 todo 的 id 会与初始的重叠,致使出现问题,因此咱们删除 src/reducers/todos.js
中对应的 initialTodoState
,而后给 todos
reducer 的 state 赋予一个 []
的默认值。
const todos = (state = [], action) => {
switch (action.type) {
// ...
}
};
export default todos;
复制代码
保存修改的内容,你会发现咱们的待办事项小应用依然能够完整的运行,可是咱们已经成功的将原来的 TodoList
分离成了容器组件的 VisibleTodoList
以及展现组件的 TodoList
了。
咱们趁热打铁,用上一节学到的知识来立刻将 Footer 组件的状态和渲染抽离。
咱们在 src/containers
文件夹下建立一个 FilterLink.js
文件,添加对应的内容以下:
import { connect } from "react-redux";
import { setVisibilityFilter } from "../actions";
import Link from "../components/Link";
const mapStateToProps = (state, ownProps) => ({
active: ownProps.filter === state.filter
});
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
});
export default connect(mapStateToProps, mapDispatchToProps)(Link);
复制代码
能够看到咱们作了如下几件工做:
mapStateToProps
,它负责比较 Redux Store 中保存的 State 的 state.filter
属性和组件接收父级传下来的 ownProps.filter
属性是否相同,若是相同,则把 active
设置为 true
。mapDispatchToProps
,它经过返回一个 onClick
函数,当组件点击时,调用生成一个 dispatch
Action,将此时组件接收父级传下来的 ownProps.filter
参数传进 setVisibilityFilter
,生成 action.type
为 "SET_VISIBILITY_FILTER"
的 Action,并 dispatch
这个 Action。connect
组合这二者,将对应的属性合并进 Link
组件并导出。咱们如今应该能够在 Link
组件中取到咱们在上面两个函数中定义的 active
和 onClick
属性了。接着咱们来编写原 Footer 的展现组件部分,打开 src/components/Footer.js
文件,对相应的内容做出以下的修改:
import React from "react";
import FilterLink from "../containers/FilterLink";
import { VisibilityFilters } from "../actions";
const Footer = () => (
<div> <span>Show: </span> <FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink> <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink> <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink> </div>
);
export default Footer;
复制代码
能够看到上面的代码修改作了这么几件工做:
Link
换成了 FilterLink
。请注意当组件的状态和渲染分离以后,咱们将使用容器组件为导出给其余组件使用的组件。FilterLink
组件,并传递对应的三个 FilterLink
过滤器类型。connect
和 setVisibilityFilter
导出。filter
和 dispatch
属性,由于它们已经在 FilterLink
中定义并传给了 Link
组件了。当咱们将 Footer 中的状态和渲染拆分以后,src/components/App.js
对应的 Footer 相关的内容就再也不须要了,咱们对文件中对应的内容做出以下修改:
import React from "react";
import AddTodo from "./AddTodo";
import VisibleTodoList from "../containers/VisibleTodoList";
import Footer from "./Footer";
class App extends React.Component {
render() {
return (
<div> <AddTodo /> <VisibleTodoList /> <Footer /> </div>
);
}
}
export default App;
复制代码
能够看到咱们作了以下工做:
App
组件中对应的 filter
属性和 mapStateToProps
函数,由于咱们已经在 FilterLink
中获取了对应的属性,因此咱们再也不须要直接从 App 组件传给 Footer 组件了。connect
函数。connect(mapStateToProps)()
,由于 App 再也不须要直接从 Redux Store 中获取内容了。保存修改的内容,你会发现咱们的待办事项小应用依然能够完整的运行,可是咱们已经成功的将原来的 Footer
分离成了容器组件的 FilterLink
以及展现组件的 Footer
了。
让咱们来完成最后一点收尾工做,将 AddTodo
组件的状态和渲染分离。
咱们在 src/containers
文件夹中建立 AddTodoContainer.js
文件,在其中添加以下内容:
import { connect } from "react-redux";
import { addTodo } from "../actions";
import AddTodo from "../components/AddTodo";
const mapStateToProps = (state, ownProps) => {
return ownProps;
};
const mapDispatchToProps = dispatch => ({
addTodo: text => dispatch(addTodo(text))
});
export default connect(mapStateToProps, mapDispatchToProps)(AddTodo);
复制代码
能够看到咱们作了几件熟悉的工做:
mapStateToProps
,由于 AddTodo
不须要从 Redux Store 中取内容,因此 mapStateToProps
只是单纯地填充 connect
的第一个参数,而后简单地返回组件的原 props
,不起其它做用。mapDispatchToProps
,咱们定义了一个 addTodo
函数,它接收 text
,而后 dispatch
一个 action.type
为 "ADD_TODO"
的 Action。connect
组合这二者,将对应的属性合并进 AddTodo
组件并导出。咱们如今应该能够在 AddTodo
组件中取到咱们在上面两个函数中定义的 addTodo
属性了。接着咱们来编写 AddTodo 的展现组件部分,打开 src/components/AddTodo.js
文件,对相应的内容做出以下的修改:
import React from "react";
const AddTodo = ({ addTodo }) => {
let input;
return (
<div> <form onSubmit={e => { e.preventDefault(); if (!input.value.trim()) { return; } addTodo(input.value); input.value = ""; }} > <input ref={node => (input = node)} /> <button type="submit">Add Todo</button> </form> </div> ); }; export default AddTodo; 复制代码
能够看到,上面的代码作了这么几件工做:
connect
函数,而且去掉了其对 AddTodo
的包裹。AddTodo
接收的属性从 dispatch
替换成从 AddTodoContainer
传过来的 addTodo
函数,当表单提交时,它将被调用,dispatch
一个 action.type
为 "ADD_TODO"
,text
为 input.value
的 Action。由于咱们将原 TodoList
分离成了容器组件 AddTodoContainer
和展现组件 TodoList
,因此咱们须要对 src/components/App.js
作出以下的修改:
import React from "react";
import AddTodoContainer from "../containers/AddTodoContainer";
import VisibleTodoList from "../containers/VisibleTodoList";
import Footer from "./Footer";
class App extends React.Component {
render() {
return (
<div> <AddTodoContainer /> <VisibleTodoList /> <Footer /> </div>
);
}
}
export default App;
复制代码
能够看到咱们使用 AddTodoContainer
替换了原来的 AddTodo
导出,并在 render
方法中渲染 AddTodoContainer
组件。
保存修改的内容,你会发现咱们的待办事项小应用依然能够完整的运行,可是咱们已经成功的将原来的 AddTodo
分离成了容器组件的 AddTodoContainer
以及展现组件的 AddTodo
了。
到目前为止,咱们就已经学习完了 Redux 的全部基础概念,而且运用这些基础概念将一个纯 React 版的待办事项一步一步重构到了 Redux。
让咱们最后一次祭出 Redux 状态循环图,回顾咱们在这篇教程中学到的知识:
咱们在这一系列教程中首先提出了 Redux 的三大概念:Store,Action,Reducers:
{ type: 'ACTION_TYPE', data1, data2 }
这样的形式声明式的定义一个 Action,而后经过 dispatch
这个 Action 来发生的。switch
语句匹配 action.type
,经过对 State 的属性进行增删改查,而后返回一个新 State 的操做。同时它也是一个纯函数,即不会直接修改 State
自己。具体反映到咱们重构的待办事项项目里,咱们使用 Store 保存的状态来替换以前 React 中的 this.state
,使用 Action 来代替以前 React 发起修改 this.state
的动做,经过 dispatch
Action 来发起修改 Store 中状态的操做,使用 Reducers 代替以前 React 中更新状态的 this.setState
操做,纯化的更新 Store 里面保存的 State。
接着咱们趁热打铁,使用以前学到的三大概念,将整个待办事情的剩下部分重构到了 Redux。
可是重构完咱们发现,咱们如今的 rootReducer
函数已经有点臃肿了,它包含了 todos
和 filter
两类不一样的状态属性,而且若是咱们想要继续扩展这个待办事项应用,那么还会继续添加不一样的状态属性,到时候各类状态属性的操做夹杂在一块儿很容易形成混乱和下降代码的可读性,不利于维护,所以咱们提出了 combineReducers
方法,用于切分 rootReducer
到多个分散在不一样文件的保存着单一状态属性的 Reducer,,而后经过 combineReducers
来组合这些拆分的 Reducers。
详细讲解 combineReducers
的概念以后,咱们接着将以前的不彻底重构的 Redux 代码进行了又一次重构,将 rootReducer
拆分红了 todos
和 filter
两个 Reducer。
最后咱们更进一步,让 React 重拾初心—— 专一于用户界面的展现,让应用的状态和渲染分离,咱们提出了展现组件和容器组件的概念,前者是完彻底全的 React,接收来自后者的数据,而后负责将数据高效正确的渲染;前者负责响应用户的操做,而后交给后者发出具体的指令,能够看到,当咱们使用 Redux 以后,咱们在 React 上盖了一层逻辑,这层逻辑彻底负责状态方面的工做,这就是 Redux 的精妙之处啊!
但愿看到这里的同窗能对 Redux 有个很好的了解,并能灵活的结合 React 和 Redux 的使用,感谢你的阅读!
细心的读者可能发现了,咱们画的 Redux 状态循环图都是单向的,它有一个明确的箭头指向,这其实也是 Redux 的哲学,即 ”单向数据流“,也是 React 社区推崇的设计模式,再加上 Reducer 的纯函数约定,这使得咱们整个应用的每一次状态更改都是能够被记录下来,而且能够重现出来,或者说状态是可预测的,它能够追根溯源的找到某一次状态的改变时由某一个 Action 发起的,因此 Redux 也被冠名为 ”可预测的状态管理容器“。
想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。