https://www.redux.org.cn/
知识铺垫 SPA(Single Page Application) 那么SPA和之前的网页有什么区别呢?其实最本质的区别就是服务端渲染
和前端渲染
的区别。
服务端渲染,是指浏览器请求一个URL地址,服务端返回的HTML页面是完整的,浏览器直接展示这个HTML内容再加上CSS的样式即可。JS主要做一些辅助性的特效或其他工作。
前端渲染,是指浏览器请求一个URL地址,服务端返回的HTML页面并不包含具体内容,而是会通过 script 标签引入一个JavaScript文件。浏览器只有通过解析和执行JS代码页面上才会展示出内容 ,否则页面就是空白的。而且后续的页面更新也都是在JS中完成,而不是通过跳转到另一个URL地址来完成(这也是单页应用名字的由来)。当然这里并不是指URL地址就一定不会变化,而是说页面不会“刷新”。
SPA的出现使得Web前端成为真正的”客户端程序”,可以独立完成渲染(DOM API)、网络请求(XHR,fetch)等任务,也就是所谓的”前端渲染”,而不是只是一个“网页展示器”。
声明式编程和命令式编程
前端状态管理之所以流行的另一个原因是,现代的前端框架包括React/Vue都是声明式的编程方式,而之前的jQuery是命令式的。
所谓声明式,就是说你在代码中不会直接去操作UI,而是通过操作数据,来间接地改变UI内容 。而命令式,是直接操作UI的,这就导致数据层和展现层通常是不作区分的。
声明式的编码方式天然的会把数据状态和页面代码分离,所以更需要一套独立的状态管理系统。
什么是状态管理
容器组件(Smart/Container Components)和展示组件(Dumb/Presentational Components) 如果将组件划分为两类,你会发现组件重用起来更加容易。我把这两类称为Container
和Presentational
。
首先我们来看一个容器组件和展示组件一起的例子吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class TodoList extends React .Component { constructor (props ) { super (props); this .state ={ todos :[] } this .fetchData = this .fetchData.bind(this ); } componentDidMount ( ) { this .fetchData(); } fetchData ( ) { fetch('/api/todos' ).then(data => { this .setState({ todos :data }) }) } render ( ) { const {todos} = this .state; return (<div > <ul > {todos.map((item,index)=>{ return <li key ={item.id} > {item.name}</li > })} </ul > </div > ) } }
大家可以看到这个例子是没有办法复用的,因为数据的请求和数据的展示都在一个组件进行,要实现组件的复用,我们就需要将展示组件和容器组件分离出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class TodoList extends React .Component { constructor (props ) { super (props); } render ( ) { const {todos} = this .props; return (<div > <ul > {todos.map((item,index)=>{ return <li key ={item.id} > {item.name}</li > })} </ul > </div > ) } class TodoListContainer extends React .Component { constructor (props ) { super (props); this .state = { todos :[] } this .fetchData = this .fetchData.bind(this ); } componentDidMount ( ) { this .fetchData(); } fetchData ( ) { fetch('/api/todos' ).then(data => { this .setState({ todos :data }) }) } render ( ) { return (<div > <TodoList todos ={this.state.todos} /> </div > ) } }
当我们把组件分离成容器组件和展示组件这两类时,你会发现他们能够很方便的实现复用。
presentational
组件:
关心事物如何展示;
也许会同时包含展示类组件和容器组件,并且通常有一些DOM操作和自己的样式;
通常允许this.props.chidren
放在容器里;
对应用的其余部分没有依赖,比如Flux actions
或者stores
;
不会说明数据是如何加载和变化的;
只通过props
接受数据和回调函数;
几乎没有自己的state
(即使有,也是UI
相关的,而不是data
相关的);
通常被写作函数式组件 除非需要状态、生命周期的钩子、性能优化;
例子:Page
, Sidebar
, Story
, UserInfo
, List
。
container
组件:
关心事物如何运作;
也许会同时包含展示类组件和容器组件,但是除了一些用来包含元素的div通常不会其他有DOM操作,并且不会包含任何样式;
为presentational
组件或其他container
组件提供数据和行为;
通常是有状态的,因为它们往往作为数据源;
通常使用像React Redux
的connect()
、Relay
的createContainer()
、Flux Utils
的Container.create()
这样的高阶组件 来生成,而不是手动编写;
例子:UserPage
, FollowersSidebar
, StoryContainer
, FollowedUserList
。
展示组件
容器组件
作用
描述如何展现(骨架、样式)
描述如何运行(数据获取、状态更新)
直接使用 Redux
否
是
数据来源
props
监听 Redux state
数据修改
从 props 调用回调函数
向 Redux 派发 actions
调用方式
手动
通常由 React Redux 生成
大部分的组件都应该是展示型的,但一般需要少数的几个容器组件把它们和 Redux store 连接起来 。
优点(benifit)
展示和容器组件更好的分离,有助于更好的理解应用和UI
重用性高,展示组件可以用于多个不同数据源。
展示组件就是你的调色板,可以把他们放到单独的页面,在不影响应用程序的情况下,让设计师调整UI。
这迫使您提取诸如侧边栏,页面,上下文菜单等“布局组件”并使用this.props.children,而不是在多个容器组件中复制相同的标记和布局。
介绍
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。
可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。
在下面的场景中,引入 Redux 是比较明智的:
你有着相当大量的、随时间变化的数据
你的 state 需要有一个单一可靠数据来源
你觉得把所有 state 放在最顶层组件中已经无法满足需要了
动机
随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理更多的 state (状态) 。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。
管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。
为什么需要Redux?
在React中,数据在组件中是单向流动的。数据从一个方向父组件流向子组件(通过props),由于这个特征,两个非父子关系的组件(或者称作兄弟组件)之间的通信并不是那么清楚。
React并不建议直接采用组件到组件的通信方式,尽管它有一些特性可以支持这么做(比如先将子组件的值传递给父组件,然后再由父组件在分发给指定的子组件)。这被很多人认为是糟糕的实践方式,因为这样的方式容易出错而且会让代码向“拉面”一样不容易理解。
Redux的出现就让这个问题的解决变得更加方便了。Redux提供一种存储整个应用状态到一个地方 的解决方案(可以理解为统一状态层),称为store
,组件将状态的变化转发通知(dispatch)给store,而不是直接通知其它的组件。组件内部依赖的state的变化情况可以通过订阅store来实现。
使用Redux,所有的组件都从store里面获取它们依赖的state,同时也需要将state的变化告知store。组件不需要关注在这个store里面其它组件的state的变化情况,Redux让数据流变得更加简单。这种思想最初来自Flux,它是一种和React相同的单向数据流的设计模式。
核心概念 Redux
的核心由三部分组成:Store
, Action
, Reducer
。
Store
: 是个对象,贯穿你整个应用的数据都应该存储在这里。
Action
: 是个对象,必须包含type
这个属性,reducer
将根据这个属性值来对store
进行相应的处理。
Reducer
:是个函数。接受两个参数:要修改的数据(state) 和 action
对象。根据action.type
来决定采用的操作,对state
进行修改,最后返回新的state
。
调用关系如下所示:
1 store.dispatch(action) --> reducer(state, action) --> final state
store store:
当使用普通对象来描述应用的 state 时。例如,todo 应用的 state 可能长这样:
1 2 3 4 5 6 7 8 9 10 { todos : [{ text : 'Eat food' , completed : true }, { text : 'Exercise' , completed : false }], visibilityFilter : 'SHOW_COMPLETED' }
这个对象就像 “Model”,区别是它并没有 setter(修改器方法)。因此其它的代码不能随意修改它,造成难以复现的 bug。
action 要想更新 state 中的数据,你需要发起一个 action。Action 就是一个普通 JavaScript 对象,来描述发生了什么。
1 2 3 { type : 'ADD_TODO' , text : 'Go to swimming pool' } { type : 'TOGGLE_TODO' , index : 1 } { type : 'SET_VISIBILITY_FILTER' , filter : 'SHOW_ALL' }
reduers
action 就像是描述发生了什么的指示器。最终,为了把 action 和 state 串起来,开发一些函数,这就是 reducer。
reducer 只是一个接收 state 和 action,并返回新的 state 的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function visibilityFilter (state = 'SHOW_ALL' , action ) { if (action.type === 'SET_VISIBILITY_FILTER' ) { return action.filter } else { return state } } function todos (state = [], action ) { switch (action.type) { case 'ADD_TODO' : return state.concat([{ text : action.text, completed : false }]) case 'TOGGLE_TODO' : return state.map((todo, index ) => action.index === index ? { text : todo.text, completed : !todo.completed } : todo ) default : return state } }
再开发一个reducer 调用这两个 reducer,进而来管理整个应用的 state :
1 2 3 4 5 6 function todoApp (state = {}, action ) { return { todos : todos(state.todos, action), visibilityFilter : visibilityFilter(state.visibilityFilter, action) } }
这差不多就是 Redux 思想的全部。注意到没我们还没有使用任何 Redux 的 API。Redux 里有一些工具来简化这种模式,但是主要的想法是如何根据这些 action 对象来更新 state ,而且 90% 的代码都是纯 JavaScript,没用 Redux、Redux API 和其它魔法。
联系与总结 步骤如下:
编写action对象
使用dispath函数发起action
使用reducers处理被发起的action( 函数内部可能会修改store,然后返回store的最新状态,也就是说state)
三大原则 单一数据源 整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 console .log(store.getState())
State 是只读的 唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
这样确保了视图和网络请求都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行
1 2 3 4 5 6 7 8 9 store.dispatch({ type : 'COMPLETE_TODO' , index : 1 }) store.dispatch({ type : 'SET_VISIBILITY_FILTER' , filter : 'SHOW_COMPLETED' })
使用纯函数来执行修改 为了描述 action 如何改变 state tree ,你需要编写 reducers。
纯函数:是指不依赖于 且 不改变 它作用域之外的变量状态 的函数。也就是说,纯函数的返回值只由它调用时的参数决定 ,它的执行不依赖于系统的状态(比如:何时、何处调用它。
纯函数的特点:
给定相同的输入,将始终返回相同的输出。
无副作用。意味着它无法更改任何外部状态。
1 2 3 4 5 6 7 8 const addToCart = (cart, item, quantity ) => { cart.items.push({ item, quantity }); return cart; };
Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。
刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分,因为 reducer 只是函数,你可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来处理一些通用任务,如分页器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function visibilityFilter (state = 'SHOW_ALL' , action ) { switch (action.type) { case 'SET_VISIBILITY_FILTER' : return action.filter default : return state } } function todos (state = [], action ) { switch (action.type) { case 'ADD_TODO' : return [ ...state, { text : action.text, completed : false } ] case 'COMPLETE_TODO' : return state.map((todo, index ) => { if (index === action.index) { return Object .assign({}, todo, { completed : true }) } return todo }) default : return state } } import { combineReducers, createStore } from 'redux' let reducer = combineReducers({ visibilityFilter, todos })let store = createStore(reducer)
快速入门 Action
Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一 来源。Action 本质上是 JavaScript 普通对象。
一般来说会通过 store.dispatch()
将 action 传到 store。
添加新 todo 任务的 action 是这样的:
1 2 3 4 5 6 7 8 9 const ADD_TODO = 'ADD_TODO' { type : ADD_TODO, text : 'Build my first Redux app' }
Action 创建函数 Action 创建函数 就是生成 action 的方法。这样做将使 action 创建函数更容易被移植和测试。
1 2 3 4 5 6 7 8 9 10 11 12 function addTodo (text ) { return { type : ADD_TODO, text } } dispatch(addTodo(text)) const boundAddTodo = text => dispatch(addTodo(text))boundAddTodo(text);
使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 export const ADD_TODO = 'ADD_TODO' export const TOGGLE_TODO = 'TOGGLE_TODO' export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER' export const VisibilityFilters = { SHOW_ALL : 'SHOW_ALL' , SHOW_COMPLETED : 'SHOW_COMPLETED' , SHOW_ACTIVE : 'SHOW_ACTIVE' } export function addTodo (text ) { return { type : ADD_TODO, text } } export function toggleTodo (index ) { return { type : TOGGLE_TODO, index } } export function setVisibilityFilter (filter ) { return { type : SET_VISIBILITY_FILTER, filter } }
Reducer
Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的 ,
记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。
state:
1 2 3 4 5 6 7 8 9 10 11 12 13 { visibilityFilter : 'SHOW_ALL' , todos : [ { text : 'Consider using Redux' , completed : true , }, { text : 'Keep all state in a single tree' , completed : false } ] }
Action 处理 reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。
永远不要 在 reducer 里做这些操作:
修改传入参数;
执行有副作用的操作,如 API 请求和路由跳转;
调用非纯函数,如 Date.now()
或 Math.random()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import { VisibilityFilters } from './actions' const initialState = { visibilityFilter : VisibilityFilters.SHOW_ALL, todos : [] } function todoApp (state, action ) { if (typeof state === 'undefined' ) { return initialState } return state } function todoApp (state = initialState, action ) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object .assign({}, state, { visibilityFilter : action.filter }) case ADD_TODO: return Object .assign({}, state, { todos : [ ...state.todos, { text : action.text, completed : false } ] }) case TOGGLE_TODO: return Object .assign({}, state, { todos : state.todos.map((todo, index ) => { if (index === action.index) { return Object .assign({}, todo, { completed : !todo.completed }) } return todo }) }) default : return state } }
拆分 Reducer 上面代码的 todos
和 visibilityFilter
的更新看起来是相互独立的。我们可以把 todos
更新的业务逻辑拆分到一个单独的函数里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 function todos (state = [], action ) { switch (action.type) { case ADD_TODO: return [ ...state, { text : action.text, completed : false } ] case TOGGLE_TODO: return state.map((todo, index ) => { if (index === action.index) { return Object .assign({}, todo, { completed : !todo.completed }) } return todo }) default : return state } } function todoApp (state = initialState, action ) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object .assign({}, state, { visibilityFilter : action.filter }) case ADD_TODO: return Object .assign({}, state, { todos : todos(state.todos, action) }) case TOGGLE_TODO: return Object .assign({}, state, { todos : todos(state.todos, action) }) default : return state } }
能否抽出一个 reducer 来专门管理 visibilityFilter
?当然可以:
注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state
参数都不同,分别对应它管理的那部分 state 数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 const { SHOW_ALL } = VisibilityFiltersfunction todos (state = [], action ) { switch (action.type) { case ADD_TODO: return [ ...state, { text : action.text, completed : false } ] case TOGGLE_TODO: return state.map((todo, index ) => { if (index === action.index) { return Object .assign({}, todo, { completed : !todo.completed }) } return todo }) default : return state } } function visibilityFilter (state = SHOW_ALL, action ) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default : return state } } function todoApp (state = {}, action ) { return { visibilityFilter : visibilityFilter(state.visibilityFilter, action), todos : todos(state.todos, action) } }
这就是所谓的reducer 合成
,它是开发 Redux 应用最基础的模式。
combineReducers() combineReducers()
所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理 ,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。没有任何魔法。 正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都没有更改 state,那么也就不会创建一个新的对象。
最后,Redux 提供了 combineReducers()
工具类来做上面 todoApp
做的事情,这样就能消灭一些样板代码了。有了它,可以这样重构 todoApp
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { combineReducers } from 'redux' const todoApp = combineReducers({ visibilityFilter, todos }) export default todoAppexport default function todoApp (state = {}, action ) { return { visibilityFilter : visibilityFilter(state.visibilityFilter, action), todos : todos(state.todos, action) } }
你也可以给它们设置不同的 key,或者调用不同的函数。下面两种合成 reducer 方法完全等价:
1 2 3 4 5 6 7 8 9 10 11 12 13 const reducer = combineReducers({ a : doSomethingWithA, b : processB, c : c }) function reducer (state = {}, action ) { return { a : doSomethingWithA(state.a, action), b : processB(state.b, action), c : c(state.c, action) } }
使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import { combineReducers } from 'redux' import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions' const { SHOW_ALL } = VisibilityFiltersfunction visibilityFilter (state = SHOW_ALL, action ) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default : return state } } function todos (state = [], action ) { switch (action.type) { case ADD_TODO: return [ ...state, { text : action.text, completed : false } ] case TOGGLE_TODO: return state.map((todo, index ) => { if (index === action.index) { return Object .assign({}, todo, { completed : !todo.completed }) } return todo }) default : return state } } const todoApp = combineReducers({ visibilityFilter, todos }) export default todoApp
Store
使用 action 来描述“发生了什么”,
使用 reducers 来根据 action 更新 state 的用法。
Store 就是把action和reducers联系到一起的对象。Store 有以下职责:
根据已有的 reducer 来创建 store
1 2 3 import { createStore } from 'redux' import todoApp from './reducers' let store = createStore(todoApp)
发起 Actions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions' console .log(store.getState())const unsubscribe = store.subscribe(() => console .log(store.getState()))store.dispatch(addTodo('Learn about actions' )) store.dispatch(addTodo('Learn about reducers' )) store.dispatch(addTodo('Learn about store' )) store.dispatch(toggleTodo(0 )) store.dispatch(toggleTodo(1 )) store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED)) unsubscribe()
数据流 严格的单向数据流 是 Redux 架构的设计核心。
这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。
同时也鼓励做数据范式化,这样可以避免使用多个且独立的无法相互引用的重复数据。
Redux 应用中数据的生命周期
调用 store.dispatch(action)。
Redux store 调用传入的 reducer 函数。
根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
Redux store 保存了根 reducer 返回的完整 state 树。
搭配 React
Redux 和 React 搭配起来用很好,因为这类库允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。
实现展示组件 它们只是普通的 React 组件。我们会使用函数式无状态组件,除非需要本地 state 或生命周期函数的场景。这并不是说展示组件必须是函数 – 只是因为这样做容易些。如果你需要使用本地 state,生命周期方法,或者性能优化,可以将它们转成 class。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import React from 'react' import PropTypes from 'prop-types' const Todo = ({ onClick, completed, text } ) => ( <li onClick ={onClick} style ={{ textDecoration: completed ? 'line-through ' : 'none ' }} > {text} </li > ) Todo.propTypes = { onClick : PropTypes.func.isRequired, completed : PropTypes.bool.isRequired, text : PropTypes.string.isRequired } export default Todo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import React from 'react' import PropTypes from 'prop-types' import Todo from './Todo' const TodoList = ({ todos, onTodoClick } ) => ( <ul > {todos.map((todo, index) => ( <Todo key ={index} {...todo } onClick ={() => onTodoClick(index)} /> ))} </ul > ) TodoList.propTypes = { todos : PropTypes.arrayOf( PropTypes.shape({ id : PropTypes.number.isRequired, completed : PropTypes.bool.isRequired, text : PropTypes.string.isRequired }).isRequired ).isRequired, onTodoClick : PropTypes.func.isRequired } export default TodoList
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import React from 'react' import PropTypes from 'prop-types' const Link = ({ active, children, onClick } ) => { if (active) { return <span > {children}</span > } return ( <a href ="" onClick ={e => { e.preventDefault() onClick() }} > {children} </a > ) } Link.propTypes = { active : PropTypes.bool.isRequired, children : PropTypes.node.isRequired, onClick : PropTypes.func.isRequired } export default Link
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import React from 'react' import FilterLink from '../containers/FilterLink' const Footer = () => ( <p > Show: <FilterLink filter ="SHOW_ALL" > All</FilterLink > {', '} <FilterLink filter ="SHOW_ACTIVE" > Active</FilterLink > {', '} <FilterLink filter ="SHOW_COMPLETED" > Completed</FilterLink > </p > ) export default Footer
实现容器组件
现在来创建一些容器组件把这些展示组件和 Redux 关联起来。
技术上讲,容器组件就是使用 store.subscribe()
从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。
你可以手工来开发容器组件,但建议使用 React Redux 库的 connect()
方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。
使用 connect()
前,需要先定义 mapStateToProps
这个函数来指定如何把当前 Redux store state 映射到展示组件的 props 中。例如,VisibleTodoList
需要计算传到 TodoList
中的 todos
,所以定义了根据 state.visibilityFilter
来过滤 state.todos
的方法,并在 mapStateToProps
中使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const getVisibleTodos = (todos, filter ) => { switch (filter) { case 'SHOW_COMPLETED' : return todos.filter(t => t.completed) case 'SHOW_ACTIVE' : return todos.filter(t => !t.completed) case 'SHOW_ALL' : default : return todos } } const mapStateToProps = state => { return { todos : getVisibleTodos(state.todos, state.visibilityFilter) } }
除了读取 state,容器组件还能分发 action 。类似的方式,可以定义 mapDispatchToProps()
方法接收 dispatch()
方法并返回期望注入到展示组件的 props 中的回调方法。例如,我们希望 VisibleTodoList
向 TodoList
组件中注入一个叫 onTodoClick
的 props ,还希望 onTodoClick
能分发 TOGGLE_TODO
这个 action:
1 2 3 4 5 6 7 const mapDispatchToProps = dispatch => { return { onTodoClick : id => { dispatch(toggleTodo(id)) } } }
最后,使用 connect()
创建 VisibleTodoList
,并传入这两个函数。
1 2 3 4 5 6 7 8 import { connect } from 'react-redux' const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList) export default VisibleTodoList
这就是 React Redux API 的基础。
其它容器组件定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { connect } from 'react-redux' import { setVisibilityFilter } from '../actions' import Link from '../components/Link' const mapStateToProps = (state, ownProps ) => { return { active : ownProps.filter === state.visibilityFilter } } const mapDispatchToProps = (dispatch, ownProps ) => { return { onClick : () => { dispatch(setVisibilityFilter(ownProps.filter)) } } } const FilterLink = connect( mapStateToProps, mapDispatchToProps )(Link) export default FilterLink
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import { connect } from 'react-redux' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList' const getVisibleTodos = (todos, filter ) => { switch (filter) { case 'SHOW_ALL' : return todos case 'SHOW_COMPLETED' : return todos.filter(t => t.completed) case 'SHOW_ACTIVE' : return todos.filter(t => !t.completed) } } const mapStateToProps = state => { return { todos : getVisibleTodos(state.todos, state.visibilityFilter) } } const mapDispatchToProps = dispatch => { return { onTodoClick : id => { dispatch(toggleTodo(id)) } } } const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList) export default VisibleTodoList
其它组件 AddTodo
组件的视图和逻辑混合在一个单独的定义之中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import React from 'react' import { connect } from 'react-redux' import { addTodo } from '../actions' let AddTodo = ({ dispatch } ) => { let input return ( <div > <form onSubmit ={e => { e.preventDefault() if (!input.value.trim()) { return } dispatch(addTodo(input.value)) input.value = '' }} > <input ref ={node => { input = node }} /> <button type ="submit" > Add Todo</button > </form > </div > ) } AddTodo = connect()(AddTodo) export default AddTodo
将容器放到一个组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import React from 'react' import Footer from './Footer' import AddTodo from '../containers/AddTodo' import VisibleTodoList from '../containers/VisibleTodoList' const App = () => ( <div > <AddTodo /> <VisibleTodoList /> <Footer /> </div > ) export default App
传入 Store 所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store
把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了一个容器组件。
建议的方式是使用指定的 React Redux 组件<Provider>
来 魔法般的 让所有容器组件都可以访问 store,而不必显式地传递它。只需要在渲染根组件时使用即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import { createStore } from 'redux' import todoApp from './reducers' import App from './components/App' let store = createStore(todoApp)render( <Provider store ={store} > <App /> </Provider > , document .getElementById('root' ) )
示例: Todo 列表 入口文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import { createStore } from 'redux' import rootReducer from './reducers' import App from './components/App' const store = createStore(rootReducer)render( <Provider store ={store} > <App /> </Provider > , document .getElementById('root' ) )
创建 Action 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let nextTodoId = 0 export const addTodo = text => ({ type : 'ADD_TODO' , id : nextTodoId++, text }) export const setVisibilityFilter = filter => ({ type : 'SET_VISIBILITY_FILTER' , filter }) export const toggleTodo = id => ({ type : 'TOGGLE_TODO' , id }) export const VisibilityFilters = { SHOW_ALL : 'SHOW_ALL' , SHOW_COMPLETED : 'SHOW_COMPLETED' , SHOW_ACTIVE : 'SHOW_ACTIVE' }
Reducers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const todos = (state = [], 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
1 2 3 4 5 6 7 8 9 10 11 const visibilityFilter = (state = 'SHOW_ALL' , action ) => { switch (action.type) { case 'SET_VISIBILITY_FILTER' : return action.filter default : return state } } export default visibilityFilter
1 2 3 4 5 6 7 8 9 import { combineReducers } from 'redux' import todos from './todos' import visibilityFilter from './visibilityFilter' export default combineReducers({ todos, visibilityFilter })
展示组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import React from 'react' import PropTypes from 'prop-types' const Todo = ({ onClick, completed, text } ) => ( <li onClick ={onClick} style ={{ textDecoration: completed ? 'line-through ' : 'none ' }} > {text} </li > ) Todo.propTypes = { onClick : PropTypes.func.isRequired, completed : PropTypes.bool.isRequired, text : PropTypes.string.isRequired } export default Todo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import React from 'react' import PropTypes from 'prop-types' const Link = ({ active, children, onClick } ) => ( <button onClick ={onClick} disabled ={active} style ={{ marginLeft: '4px ' }} > {children} </button > ) Link.propTypes = { active : PropTypes.bool.isRequired, children : PropTypes.node.isRequired, onClick : PropTypes.func.isRequired } export default Link
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import React from 'react' import Footer from './Footer' import AddTodo from '../containers/AddTodo' import VisibleTodoList from '../containers/VisibleTodoList' const App = () => ( <div > <AddTodo /> <VisibleTodoList /> <Footer /> </div > ) export default App
容器组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { connect } from 'react-redux' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList' const getVisibleTodos = (todos, filter ) => { switch (filter) { case 'SHOW_COMPLETED' : return todos.filter(t => t.completed) case 'SHOW_ACTIVE' : return todos.filter(t => !t.completed) case 'SHOW_ALL' : default : return todos } } const mapStateToProps = state => ({ todos : getVisibleTodos(state.todos, state.visibilityFilter) }) const mapDispatchToProps = dispatch => ({ toggleTodo : id => dispatch(toggleTodo(id)) }) export default connect( mapStateToProps, mapDispatchToProps )(TodoList)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { connect } from 'react-redux' import { setVisibilityFilter } from '../actions' import Link from '../components/Link' const mapStateToProps = (state, ownProps ) => ({ active : ownProps.filter === state.visibilityFilter }) const mapDispatchToProps = (dispatch, ownProps ) => ({ onClick : () => dispatch(setVisibilityFilter(ownProps.filter)) }) export default connect( mapStateToProps, mapDispatchToProps )(Link)
其他组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import React from 'react' import { connect } from 'react-redux' import { addTodo } from '../actions' const AddTodo = ({ dispatch } ) => { let input return ( <div > <form onSubmit ={e => { e.preventDefault() if (!input.value.trim()) { return } dispatch(addTodo(input.value)) input.value = '' }} > <input ref ={node => (input = node)} /> <button type ="submit" > Add Todo</button > </form > </div > ) } export default connect()(AddTodo)