# 搭配 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/gaearon/react-redux),需要單獨安裝。
~~~
npm install --save react-redux
~~~
### 智能組件(Smart Components)和笨拙組件(Dumb Components)
Redux 的 React 綁定庫擁抱了 [“智能”組件和“笨拙”組件相分離](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 的開發思想。
明智的做法是只在最頂層組件(如路由操作)里使用 Redux。內部組件應該像木偶一樣保持“呆滯”,所有數據都通過 props 傳入。
| | 位置 | 使用 Redux | 讀取數據 | 修改數據 |
|-----|-----|-----|-----|-----|
| “智能”組件 | 最頂層,路由處理 | 是 | 從 Redux 獲取 state | 向 Redux 發起 actions |
| “笨拙”組件 | 中間和子組件 | 否 | 從 props 獲取數據 | 從 props 調用回調函數 |
在這個 todo 應用中,只應有一個“智能”組件,它存在于組件的最頂層。在復雜的應用中,也有可能會有多個智能組件。雖然你也可以嵌套使用“智能”組件,但應該盡可能的使用傳遞 props 的形式。
### 設計組件層次結構
還記得當初如何 [設計 reducer 結構](#) 嗎?現在就要定義與它匹配的界面的層次結構。其實這不是 Redux 相關的工作,[React 開發思想](https://facebook.github.io/react/docs/thinking-in-react.html)在這方面解釋的非常棒。
我們的概要設計很簡單。我們想要顯示一個 todo 項的列表。一個 todo 項被點擊后,會增加一條刪除線并標記 completed。我們會顯示用戶新增一個 todo 字段。在 footer 里顯示一個可切換的顯示全部/只顯示 completed 的/只顯示 incompleted 的 todos。
以下的這些組件(和它們的 props )就是從這個設計里來的:
- **`AddTodo`** 輸入字段的輸入框和按鈕。
- `onAddClick(text: string)` 當按鈕被點擊時調用的回調函數。
- **`TodoList`** 用于顯示 todos 列表。
- `todos: Array` 以 `{ text, completed }` 形式顯示的 todo 項數組。
- `onTodoClick(index: number)` 當 todo 項被點擊時調用的回調函數。
- **`Todo`** 一個 todo 項。
- `text: string` 顯示的文本內容。
- `completed: boolean` todo 項是否顯示刪除線。
- `onClick()` 當 todo 項被點擊時調用的回調函數。
- **`Footer`** 一個允許用戶改變可見 todo 過濾器的組件。
- `filter: string` 當前的過濾器為: `'SHOW_ALL'`、 `'SHOW_COMPLETED'` 或 `'SHOW_ACTIVE'`。
- `onFilterChange(nextFilter: string)`: 當用戶選擇不同的過濾器時調用的回調函數。
這些全部都是“笨拙”的組件。它們不知道數據是**從**哪里來的,或者數據是**怎么**變化的。你傳入什么,它們就渲染什么。
如果你要把 Redux 遷移到別的上,你應該要保持這些組件的一致性。因為它們不依賴 Redux。
直接寫就是了!我們已經不用綁定到 Redux。你可以在開發過程中給出一些實驗數據,直到它們渲染對了。
### 笨拙組件
這就是普通的 React 組件,所以就不在詳述。直接看代碼:
#### `components/AddTodo.js`
~~~
import React, { findDOMNode, Component, PropTypes } from 'react';
export default class AddTodo extends Component {
render() {
return (
<div>
<input type='text' ref='input' />
<button onClick={e => this.handleClick(e)}>
Add
</button>
</div>
);
}
handleClick(e) {
const node = findDOMNode(this.refs.input);
const text = node.value.trim();
this.props.onAddClick(text);
node.value = '';
}
}
AddTodo.propTypes = {
onAddClick: PropTypes.func.isRequired
};
~~~
#### `components/Todo.js`
~~~
import React, { Component, PropTypes } from 'react';
export default class Todo extends Component {
render() {
return (
<li
onClick={this.props.onClick}
style={{
textDecoration: this.props.completed ? 'line-through' : 'none',
cursor: this.props.completed ? 'default' : 'pointer'
}}>
{this.props.text}
</li>
);
}
}
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
};
~~~
#### `components/TodoList.js`
~~~
import React, { Component, PropTypes } from 'react';
import Todo from './Todo';
export default class TodoList extends Component {
render() {
return (
<ul>
{this.props.todos.map((todo, index) =>
<Todo {...todo}
key={index}
onClick={() => this.props.onTodoClick(index)} />
)}
</ul>
);
}
}
TodoList.propTypes = {
onTodoClick: PropTypes.func.isRequired,
todos: PropTypes.arrayOf(PropTypes.shape({
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
}).isRequired).isRequired
};
~~~
#### `components/Footer.js`
~~~
import React, { Component, PropTypes } from 'react';
export default class Footer extends Component {
renderFilter(filter, name) {
if (filter === this.props.filter) {
return name;
}
return (
<a href='#' onClick={e => {
e.preventDefault();
this.props.onFilterChange(filter);
}}>
{name}
</a>
);
}
render() {
return (
<p>
Show:
{' '}
{this.renderFilter('SHOW_ALL', 'All')}
{', '}
{this.renderFilter('SHOW_COMPLETED', 'Completed')}
{', '}
{this.renderFilter('SHOW_ACTIVE', 'Active')}
.
</p>
);
}
}
Footer.propTypes = {
onFilterChange: PropTypes.func.isRequired,
filter: PropTypes.oneOf([
'SHOW_ALL',
'SHOW_COMPLETED',
'SHOW_ACTIVE'
]).isRequired
};
~~~
就這些,現在開發一個笨拙型的組件 `App` 把它們渲染出來,驗證下是否工作。
#### `containers/App.js`
~~~
import React, { Component } from 'react';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import Footer from '../components/Footer';
export default class App extends Component {
render() {
return (
<div>
<AddTodo
onAddClick={text =>
console.log('add todo', text)
} />
<TodoList
todos={[{
text: 'Use Redux',
completed: true
}, {
text: 'Learn to connect it to React',
completed: false
}]}
onTodoClick={todo =>
console.log('todo clicked', todo)
} />
<Footer
filter='SHOW_ALL'
onFilterChange={filter =>
console.log('filter change', filter)
} />
</div>
);
}
}
~~~
渲染 `<App />` 結果如下:

單獨來看,并沒有什么特別,現在把它和 Redux 連起來。
### 連接到 Redux
我們需要做出兩個變化,將 `App` 組件連接到 Redux 并且讓它能夠 dispatch actions 以及從 Redux store 讀取到 state。
首先,我們需要獲取從之前安裝好的 [`react-redux`](http://github.com/gaearon/react-redux) 提供的 `Provider`,并且在渲染之前**將根組件包裝進 `<Provider>`**。
#### `index.js`
~~~
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './containers/App';
import todoApp from './reducers';
let store = createStore(todoApp);
let rootElement = document.getElementById('root');
React.render(
// 為了解決在 React 0.13 的一個問題
// 子標簽必須包裝成一個 function。
<Provider store={store}>
{() => <App />}
</Provider>,
rootElement
);
~~~
這使得我們的 store 能為下面的組件所用。(在內部,這個是通過 React 的 ["context" 特性](http://facebook.github.io/react/docs/context.html)實現。)
接著,我們**想要通過 [`react-redux`](http://github.com/gaearon/react-redux) 提供的 `connect()` 方法將包裝好的組件連接到Redux**。盡量只做一個頂層的組件,或者 route 處理。從技術上來說你可以將應用中的任何一個組件 `connect()` 到 Redux store 中,但盡量要避免這么做,因為這個數據流很難追蹤。
**任何一個從 `connect()` 包裝好的組件都可以得到一個 [`dispatch`](#) 方法作為組件的 props。**`connect()` 的唯一參數是 **selector**。此方法可以從 Redux store 接收到全局的 state,然后返回一個你的組件中需要的 props。最簡單的情況下,可以返回一個初始的 `state` ,但你可能希望它發生了變化。
為了組合 selectors 更有效率,不妨看看 [reselect](https://github.com/faassen/reselect)。在這個例子中我們不會用到它,但它適合更大的應用。
#### `containers/App.js`
~~~
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import Footer from '../components/Footer';
class App extends Component {
render() {
// Injected by connect() call:
const { dispatch, visibleTodos, visibilityFilter } = this.props;
return (
<div>
<AddTodo
onAddClick={text =>
dispatch(addTodo(text))
} />
<TodoList
todos={this.props.visibleTodos}
onTodoClick={index =>
dispatch(completeTodo(index))
} />
<Footer
filter={visibilityFilter}
onFilterChange={nextFilter =>
dispatch(setVisibilityFilter(nextFilter))
} />
</div>
);
}
}
App.propTypes = {
visibleTodos: PropTypes.arrayOf(PropTypes.shape({
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
})),
visibilityFilter: PropTypes.oneOf([
'SHOW_ALL',
'SHOW_COMPLETED',
'SHOW_ACTIVE'
]).isRequired
};
function selectTodos(todos, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos;
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(todo => todo.completed);
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(todo => !todo.completed);
}
}
// Which props do we want to inject, given the global state?
// Note: use https://github.com/faassen/reselect for better performance.
function select(state) {
return {
visibleTodos: selectTodos(state.todos, state.visibilityFilter),
visibilityFilter: state.visibilityFilter
};
}
// Wrap the component to inject dispatch and state into it
export default connect(select)(App);
~~~
到此為止,迷你型的任務管理應用就開發完畢。
### 下一步
參照 [本示例完整](#) 來深化理解。然后就可以跳到 [高級教程](#) 學習網絡請求處理和路由。