搭配 React
这里需要再强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。
尽管如此,Redux 还是和 React 和 Deku 这类库搭配起来用最好,因为这类库允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。
下面使用 React 来开发一个 todo 任务管理应用。
安装 React Redux
Redux 默认并不包含 React 绑定库,需要单独安装。
npm install --save react-redux
如果你不使用npm,你也可以从unpkg获取最新的UMD包(包括开发环境包和生产环境包)。如果你用 <script>
标签的方式引入UMD包,那么它会在全局抛出window.ReactRedux
对象。
容器组件(Smart/Container Components)和展示组件(Dumb/Presentational Components)
Redux 的 React 绑定库是基于 容器组件和展示组件相分离 的开发思想。所以建议先读完这篇文章再回来继续学习。这个思想非常重要。
已经读完了?那让我们再总结一下不同点:
展示组件 | 容器组件 | |
---|---|---|
作用 | 描述如何展现(骨架、样式) | 描述如何运行(数据获取、状态更新) |
直接使用 Redux | 否 | 是 |
数据来源 | props | 监听 Redux state |
数据修改 | 从 props 调用回调函数 | 向 Redux 派发 actions |
调用方式 | 手动 | 通常由 React Redux 生成 |
大部分的组件都应该是展示型的,但一般需要少数的几个容器组件把它们和 Redux store 连接起来。这和下面的设计简介并不意味着容器组件必须位于组件树的最顶层。如果一个容器组件变得太复杂(例如,它有大量的嵌套组件以及传递数不尽的回调函数),那么在组件树中引入另一个容器,就像FAQ中提到的那样
技术上讲你可以直接使用 store.subscribe()
来编写容器组件。但不建议这么做的原因是无法使用 React Redux 带来的性能优化。也因此,不要手写容器组件,而使用 React Redux 的 connect()
方法来生成,后面会详细介绍。
设计组件层次结构
还记得当初如何 设计 state 根对象的结构 吗?现在就要定义与它匹配的界面的层次结构。其实这不是 Redux 相关的工作,React 开发思想在这方面解释的非常棒。
我们的概要设计很简单。我们想要显示一个 todo 项的列表。一个 todo 项被点击后,会增加一条删除线并标记 completed。我们会显示用户新增一个 todo 字段。在 footer 里显示一个可切换的显示全部/只显示 completed 的/只显示 incompleted 的 todos。
展示组件
以下的这些组件(和它们的 props )就是从这个设计里来的:
TodoList
用于显示 todos 列表。todos: Array
以{ text, completed }
形式显示的 todo 项数组。onTodoClick(index: number)
当 todo 项被点击时调用的回调函数。
Todo
一个 todo 项。text: string
显示的文本内容。completed: boolean
todo 项是否显示删除线。onClick()
当 todo 项被点击时调用的回调函数。
Link
带有 callback 回调功能的链接onClick()
当点击链接时会触发
Footer
一个允许用户改变可见 todo 过滤器的组件。App
根组件,渲染余下的所有内容。
这些组件只定义外观并不关心数据来源和如何改变。传入什么就渲染什么。如果你把代码从 Redux 迁移到别的架构,这些组件可以不做任何改动直接使用。它们并不依赖于 Redux。
容器组件
还需要一些容器组件来把展示组件连接到 Redux。例如,展示型的 TodoList
组件需要一个类似 VisibleTodoList
的容器来监听 Redux store 变化并处理如何过滤出要显示的数据。为了实现状态过滤,需要实现 FilterLink
的容器组件来渲染 Link
并在点击时触发对应的 action:
VisibleTodoList
根据当前显示的状态来对 todo 列表进行过滤,并渲染TodoList
。FilterLink
得到当前过滤器并渲染Link
。filter: string
就是当前过滤的状态
其它组件
有时很难分清到底该使用容器组件还是展示组件。例如,有时表单和函数严重耦合在一起,如这个小的组件:
AddTodo
含有“Add”按钮的输入框
技术上讲可以把它分成两个组件,但一开始就这么做有点早。在一些非常小的组件里混用容器和展示是可以的。当业务变复杂后,如何拆分就很明显了。所以现在就使用混合型的吧。
组件编码
终于开始开发组件了!先做展示组件,这样可以先不考虑 Redux。
实现展示组件
它们只是普通的 React 组件,所以不会详细解释。我们会使用函数式无状态组件除非需要本地 state 或生命周期函数的场景。这并不是说展示组件必须是函数 -- 只是因为这样做容易些。如果你需要使用本地 state,生命周期方法,或者性能优化,可以将它们转成 class。
components/Todo.js
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
components/TodoList.js
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
components/Link.js
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
components/Footer.js
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()
方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。(这样你就不必为了性能而手动实现 React 性能优化建议 中的 shouldComponentUpdate
方法。)
使用 connect()
前,需要先定义 mapStateToProps
这个函数来指定如何把当前 Redux store state 映射到展示组件的 props 中。例如,VisibleTodoList
需要计算传到 TodoList
中的 todos
,所以定义了根据 state.visibilityFilter
来过滤 state.todos
的方法,并在 mapStateToProps
中使用。
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:
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
最后,使用 connect()
创建 VisibleTodoList
,并传入这两个函数。
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
这就是 React Redux API 的基础,但还漏了一些快捷技巧和强大的配置。建议你仔细学习 它的文档。如果你担心 mapStateToProps
创建新对象太过频繁,可以学习如何使用 reselect 来 计算衍生数据。
其它容器组件定义如下:
containers/FilterLink.js
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
containers/VisibleTodoList.js
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
其它组件
containers/AddTodo.js
回想一下前面提到的, AddTodo
组件的视图和逻辑混合在一个单独的定义之中。
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
如果你不熟悉ref属性, 请阅读这篇文档以熟悉这个属性的推荐用法。
将容器放到一个组件
components/App.js
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,而不必显示地传递它。只需要在渲染根组件时使用即可。
index.js
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')
)