# 搭配 React
這里需要再強調一下:Redux 和 React 之間沒有關系。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。
盡管如此,Redux 還是和 [React](http://facebook.github.io/react/) 和 [Deku](https://github.com/dekujs/deku) 這類庫搭配起來用最好,因為這類庫允許你以 state 函數的形式來描述界面,Redux 通過 action 的形式來發起 state 變化。
下面使用 React 來開發一個 todo 任務管理應用。
## 安裝 React Redux
Redux 默認并不包含 [React 綁定庫](https://github.com/reactjs/react-redux),需要單獨安裝。
```
npm install --save react-redux
```
如果你不使用 npm,你也可以從 unpkg 獲取最新的 UMD 包(包括[開發環境包](https://unpkg.com/react-redux@latest/dist/react-redux.js)和[生產環境包](https://unpkg.com/react-redux@latest/dist/react-redux.min.js))。如果你用 `<script>` 標簽的方式引入 UMD 包,那么它會在全局拋出`window.ReactRedux`對象。
## 容器組件(Smart/Container Components)和展示組件(Dumb/Presentational Components)
Redux 的 React 綁定庫是基于 [容器組件和展示組件相分離](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 的開發思想。所以建議先讀完這篇文章再回來繼續學習。這個思想非常重要。
已經讀完了?那讓我們再總結一下不同點:
<table>
<thead>
<tr>
<th></th>
<th scope="col" style="text-align:left">展示組件</th>
<th scope="col" style="text-align:left">容器組件</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" style="text-align:right">作用</th>
<td>描述如何展現(骨架、樣式)</td>
<td>描述如何運行(數據獲取、狀態更新)</td>
</tr>
<tr>
<th scope="row" style="text-align:right">直接使用 Redux</th>
<td>否</th>
<td>是</th>
</tr>
<tr>
<th scope="row" style="text-align:right">數據來源</th>
<td>props</td>
<td>監聽 Redux state</td>
</tr>
<tr>
<th scope="row" style="text-align:right">數據修改</th>
<td>從 props 調用回調函數</td>
<td>向 Redux 派發 actions</td>
</tr>
<tr>
<th scope="row" style="text-align:right">調用方式</th>
<td>手動</td>
<td>通常由 React Redux 生成</td>
</tr>
</tbody>
</table>
大部分的組件都應該是展示型的,但一般需要少數的幾個容器組件把它們和 Redux store 連接起來。這和下面的設計簡介并不意味著容器組件必須位于組件樹的最頂層。如果一個容器組件變得太復雜(例如,它有大量的嵌套組件以及傳遞數不盡的回調函數),那么在組件樹中引入另一個容器,就像[FAQ](../faq/ReactRedux.md#react-multiple-components)中提到的那樣
技術上講你可以直接使用 `store.subscribe()` 來編寫容器組件。但不建議這么做的原因是無法使用 React Redux 帶來的性能優化。也因此,不要手寫容器組件,而使用 React Redux 的 `connect()` 方法來生成,后面會詳細介紹。
## 設計組件層次結構
還記得當初如何 [設計 state 根對象的結構](Reducers.md) 嗎?現在就要定義與它匹配的界面的層次結構。其實這不是 Redux 相關的工作,[React 開發思想](https://facebook.github.io/react/docs/thinking-in-react.html)在這方面解釋的非常棒。
我們的概要設計很簡單。我們想要顯示一個 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`
```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`
```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`
```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`
```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()`](../api/Store.md#subscribe) 從 Redux state 樹中讀取部分數據,并通過 props 來把這些數據提供給要渲染的組件。你可以手工來開發容器組件,但建議使用 React Redux 庫的 [`connect()`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) 方法來生成,這個方法做了性能優化來避免很多不必要的重復渲染。(這樣你就不必為了性能而手動實現 [React 性能優化建議](https://doc.react-china.org/docs/optimizing-performance.html) 中的 `shouldComponentUpdate` 方法。)
使用 `connect()` 前,需要先定義 `mapStateToProps` 這個函數來指定如何把當前 Redux store state 映射到展示組件的 props 中。例如,`VisibleTodoList` 需要計算傳到 `TodoList` 中的 `todos`,所以定義了根據 `state.visibilityFilter` 來過濾 `state.todos` 的方法,并在 `mapStateToProps` 中使用。
```js
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()`](../api/Store.md#dispatch) 方法并返回期望注入到展示組件的 props 中的回調方法。例如,我們希望 `VisibleTodoList` 向 `TodoList` 組件中注入一個叫 `onTodoClick` 的 props ,還希望 `onTodoClick` 能分發 `TOGGLE_TODO` 這個 action:
```js
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
```
最后,使用 `connect()` 創建 `VisibleTodoList`,并傳入這兩個函數。
```js
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
```
這就是 React Redux API 的基礎,但還漏了一些快捷技巧和強大的配置。建議你仔細學習 [它的文檔](https://github.com/reactjs/react-redux)。如果你擔心 `mapStateToProps` 創建新對象太過頻繁,可以學習如何使用 [reselect](https://github.com/reactjs/reselect) 來 [計算衍生數據](../recipes/ComputingDerivedData.md)。
其它容器組件定義如下:
#### `containers/FilterLink.js`
```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`
```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` 組件的視圖和邏輯混合在一個單獨的定義之中。
```js
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 屬性, 請閱讀這篇[文檔](https://facebook.github.io/react/docs/refs-and-the-dom.html)以熟悉這個屬性的推薦用法。
### 將容器放到一個組件
#### `components/App.js`
```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>`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#provider-store) 來 [魔法般的](https://doc.react-china.org/docs/context.html) 讓所有容器組件都可以訪問 store,而不必顯式地傳遞它。只需要在渲染根組件時使用即可。
#### `index.js`
```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')
)
```
## 下一步
參照 [本完整示例](ExampleTodoList.md) 來深化理解。然后就可以跳到 [高級教程](../advanced/README.md) 學習網絡請求處理和路由。
- 自述
- 介紹
- 動機
- 核心概念
- 三大原則
- 先前技術
- 學習資源
- 生態系統
- 示例
- 基礎
- Action
- Reducer
- Store
- 數據流
- 搭配 React
- 示例:Todo List
- 高級
- 異步 Action
- 異步數據流
- Middleware
- 搭配 React Router
- 示例:Reddit API
- 下一步
- 技巧
- 配置 Store
- 遷移到 Redux
- 使用對象展開運算符
- 減少樣板代碼
- 服務端渲染
- 編寫測試
- 計算衍生數據
- 實現撤銷重做
- 子應用隔離
- 組織 Reducer
- Reducer 基礎概念
- Reducer 基礎結構
- Reducer 邏輯拆分
- Reducer 重構示例
- combineReducers 用法
- combineReducers 進階
- State 范式化
- 管理范式化數據
- Reducer 邏輯復用
- 不可變更新模式
- 初始化 State
- 結合 Immutable.JS 使用 Redux
- 常見問題
- 綜合
- Reducer
- 組織 State
- 創建 Store
- Action
- 不可變數據
- 代碼結構
- 性能
- 設計哲學
- React Redux
- 其它
- 排錯
- 詞匯表
- API 文檔
- createStore
- Store
- combineReducers
- applyMiddleware
- bindActionCreators
- compose
- react-redux 文檔
- API
- 排錯