Redux (三) 与 React 共同工作

介绍如何在 React 程序使用 Redux,大部分内容来自官方文档


与 React 共同使用

需要在此强调的是 Redux 与 React 并没有关联,你可以在 React, Angular, Ember, jQuery, 或原生 JavaScript 中使用 Redux。

Redux 与 React,Deku 等框架共同运行能获得较好的效果是因为他们可以将 UI 描绘为带有 State 的函数,Redux 响应 Action 操作后更新 State 即可自动变更页面。

我们将会使用 React 构建一个简单的 ToDo App。

安装

Redux 默认不包含 React bindings,你需要手动安装。

1
npm install --save react-redux
1
npm install --save-dev redux-devtools

调试工具能大大增提升Redux 程序开发效率。

1
2
3

npm install --save-dev redux-devtools-log-monitor
npm install --save-dev redux-devtools-dock-monitor

表现层和容器层

React bindings for Redux 遵循一个理念:分离表现层和容器层组件。如果你这个术语不熟悉,先去这里了解一下吧,他是非常重要的概念,我们在这里等你回来!

阅读完成了吗?让我们来比较他们之间的差异:

Properties Usage
Purpose How things look (markup, styles) How things work (data fetching, state updates)
Aware of Redux No Yes
To read data Read data from props Subscribe to Redux state
To change data Invoke callbacks from props Dispatch Redux actions
Are written By hand Usually generated by React Redux

通常我们写的大部分组件是表现层组件,有时我们需要少量容器组件连接表现层组件和 Redux。

设计组件层次结构

还记得我们是如何设计根 State 对象的吗?现在我们来设计与他关联的 UI 层次结构。这个任务与 Redux 不相关,Thinking in React 是关于这方面很好的教程。

我们的设计非常简洁简单,我们想显示一系列任务项的列表。点击后任务项后将他划掉代表已完成。显示一些字段供用户添加新的任务项。在底部,我们想放置一个切换按钮用来切换显示所有任务项,已完成的,活动的任务项之间。

表现层组件

我认为需要这些表现层组件和相应的 props :

  • TodoList is a list showing visible todos.
    • todos: Array is an array of todo items with { id, text, completed } shape.
    • onTodoClick(id: number) is a callback to invoke when a todo is clicked.
  • Todo is a single todo item.
    • text: string is the text to show.
    • completed: boolean is whether todo should appear crossed out.
    • onClick() is a callback to invoke when a todo is clicked.
  • Link is a link with a callback.
    • onClick() is a callback to invoke when link is clicked.
  • Footer is where we let the user change currently visible todos.
  • App is the root component that renders everything else.

这里只描述了外观,不知道数据来源,和如何操作数据。他们只渲染传入的值。如果你从 Redux 迁移到其他地方,你可以使这些组件保持一致,他们不依赖于 Redux。

容器组件

我们也需要一些容器组件连接表现层组件和 Redux。例如,表现层 TodoList 组件需要容器如 VisibleTodoList 以接收 Redux store 的状态变化,点击按钮时的响应处理。又例如可见项过滤器,我们会提供 FilterLink 容器组件生成 Link,点击 Link 时分发 Action。

  • VisibleTodoList filters the todos according to the current visibility filter and renders a TodoList.
  • FilterLink gets the current visibility filter and renders a Link.
    • filter: string is the visibility filter it represents.

其它组件

有时很难定义这些组件是表现层还是容器。例如,一些表单元素和函数是紧密相关的,例如下面这个小组件:

  • AddTodo is an input field with an “Add” button

技术角度我们可以分割成两个组件,但在这个阶段可能尚早。他的代码太少了,以至可以很好的混合表现层和逻辑。等代码增长后,能更清楚如何分割他,所以我们暂时让他们混合在一起。

实现组件

现在我们开始写组件的代码!我们准备从表现层组件开始,因此现在无需考虑如何绑定 Redux。

表现层组件

这些全都是普通的 React 组件,因此我不解释太多。我写的是函数式无状态组件,因为现在用不上本地 state 或生命周期方法。这意味着表现层组件就是一个函数-这是最简单定义他们的方式。如果你需要添加本地 state,生命周期方法,或性能优化,你可以使用 class 实现他们。

components/Todo.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { PropTypes } from 'react'

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

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, { PropTypes } from 'react'
import Todo from './Todo'

const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</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
import React, { PropTypes } from 'react'

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

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 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

components/App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

容器组件

现在将表现层组件通过容器组件与 Redux 连接。技术角度容器就是由 React 组件和用 store.subsribe() 读取 Redux state tree,并提供值给表现层组件的 props。你可以直接手写容器组件,但 Redux 包含大量优化所以我们建议使用 React Redux 库的 connect() 函数生成。

要使用 connect(),你需要定义特殊的函数叫做 mapStateToProps,他用作声明如何将当前 Store State 树转换并传递到表现层组件的 props。例如 VisibileTodoList 需要计算 todos 如何传递到 TodoList,所以我门定义了一个函数根据 state.visibilityFilter 过滤 state.todos,并在 mapStateToProps 使用他。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
}
}

除了读取状态,容器组件还能分发 action。与上面类似,你需要定义一个函数 mapDispatchToProps() 接收 dispatch() 方法,返回一个回调方法作为你想传递给表现层组件的 props 值。例如,我们想 VisibleTodoList 传递 onTodoClick 方法给 TodoList 组件,并且我们想让 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 的基础,我们建议你阅读这个文档了解其它的缩写和强大的选项
。在这个例子你可能担心使用 mapStateToProps 创建新的对象太过频繁,那么你可以去阅读 computing derived datareselect

下面是容器组件剩余部分:

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 { 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

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
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

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 React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'

let AddTodo = ({ dispatch }) => {
let input

return (
<div>
<input ref={node => {
input = node
}} />
<button onClick={() => {
dispatch(addTodo(input.value))
input.value = ''
}}>
Add Todo
</button>
</div>
)
}
AddTodo = connect()(AddTodo)

export default AddTodo

传递 Store

所有容器组件都需要访问 Redux store,因此他们需要 subscribe。其中一种方式是把通过 props 传递 store 到每个容器组件,然而这太乏味,甚至你还要传递 store 到表现层组件,因为他们可能组件树的层次结构可能非常深。

我们推荐的方式是使用 React Redux 提供的特殊的组件 <Provider>,他像魔法般使 store 可以在所有容器组件使用而不需要明确的传递。你只需要在根组件使用一次即可:

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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')
)

接下来

阅读这个指南完整的源代码 更好的消化你所学的知识。

然后,直奔高级指南 学习如何处理网络请求和 routing!