這里需要再強調(diào)一下:Redux 和 React 之間沒有關(guān)系。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。
盡管如此,Redux 還是和 React 和 Deku 這類框架搭配起來用最好,因為這類框架允許你以 state 函數(shù)的形式來描述界面,Redux 通過 action 的形式來發(fā)起 state 變化。
下面使用 React 來開發(fā)一個 todo 任務(wù)管理應(yīng)用。
Redux 默認并不包含 React 綁定庫,需要單獨安裝。
npm install --save react-redux
Redux 的 React 綁定庫擁抱了 “智能”組件和“笨拙”組件相分離 的開發(fā)思想。
明智的做法是只在最頂層組件(如路由操作)里使用 Redux。內(nèi)部組件應(yīng)該像木偶一樣保持“呆滯”,所有數(shù)據(jù)都通過 props 傳入。
位置 | 使用 Redux | 讀取數(shù)據(jù) | 修改數(shù)據(jù) | |
---|---|---|---|---|
“智能”組件 | 最頂層,路由處理 | 是 | 從 Redux 獲取 state | 向 Redux 發(fā)起 actions |
“笨拙”組件 | 中間和子組件 | 否 | 從 props 獲取數(shù)據(jù) | 從 props 調(diào)用回調(diào)函數(shù) |
在這個 todo 應(yīng)用中,只應(yīng)有一個“智能”組件,它存在于組件的最頂層。在復(fù)雜的應(yīng)用中,也有可能會有多個智能組件。雖然你也可以嵌套使用“智能”組件,但應(yīng)該盡可能的使用傳遞 props 的形式。
還記得當初如何 設(shè)計 reducer 結(jié)構(gòu) 嗎?現(xiàn)在就要定義與它匹配的界面的層次結(jié)構(gòu)。其實這不是 Redux 相關(guān)的工作,React 開發(fā)思想在這方面解釋的非常棒。
我們的概要設(shè)計很簡單。我們想要顯示一個 todo 項的列表。一個 todo 項被點擊后,會增加一條刪除線并標記 completed。我們會顯示用戶新增一個 todo 字段。在 footer 里顯示一個可切換的顯示全部/只顯示 completed 的/只顯示 incompleted 的 todos。
以下的這些組件(和它們的 props )就是從這個設(shè)計里來的:
AddTodo
輸入字段的輸入框和按鈕。
onAddClick(text: string)
當按鈕被點擊時調(diào)用的回調(diào)函數(shù)。
TodoList
用于顯示 todos 列表。
todos: Array
以 { text, completed }
形式顯示的 todo 項數(shù)組。onTodoClick(index: number)
當 todo 項被點擊時調(diào)用的回調(diào)函數(shù)。
Todo
一個 todo 項。
text: string
顯示的文本內(nèi)容。completed: boolean
todo 項是否顯示刪除線。onClick()
當 todo 項被點擊時調(diào)用的回調(diào)函數(shù)。
Footer
一個允許用戶改變可見 todo 過濾器的組件。
filter: string
當前的過濾器為: 'SHOW_ALL'
、 'SHOW_COMPLETED'
或 'SHOW_ACTIVE'
。onFilterChange(nextFilter: string)
: 當用戶選擇不同的過濾器時調(diào)用的回調(diào)函數(shù)。這些全部都是“笨拙”的組件。它們不知道數(shù)據(jù)是從哪里來的,或者數(shù)據(jù)是怎么變化的。你傳入什么,它們就渲染什么。
如果你要把 Redux 遷移到別的上,你應(yīng)該要保持這些組件的一致性。因為它們不依賴 Redux。
直接寫就是了!我們已經(jīng)不用綁定到 Redux。你可以在開發(fā)過程中給出一些實驗數(shù)據(jù),直到它們渲染對了。
這就是普通的 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
};
就這些,現(xiàn)在開發(fā)一個笨拙型的組件 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 />
結(jié)果如下:
單獨來看,并沒有什么特別,現(xiàn)在把它和 Redux 連起來。
我們需要做出兩個變化,將 App
組件連接到 Redux 并且讓它能夠 dispatch actions 以及從 Redux store 讀取到 state。
首先,我們需要獲取從之前安裝好的 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 能為下面的組件所用。(在內(nèi)部,這個是通過 React 的 "context" 特性實現(xiàn)。)
接著,我們想要通過 react-redux
提供的 connect()
方法將包裝好的組件連接到Redux。盡量只做一個頂層的組件,或者 route 處理。從技術(shù)上來說你可以將應(yīng)用中的任何一個組件 connect()
到 Redux store 中,但盡量要避免這么做,因為這個數(shù)據(jù)流很難追蹤。
任何一個從 connect()
包裝好的組件都可以得到一個 dispatch
方法作為組件的 props。connect()
的唯一參數(shù)是 selector。此方法可以從 Redux store 接收到全局的 state,然后返回一個你的組件中需要的 props。最簡單的情況下,可以返回一個初始的 state
,但你可能希望它發(fā)生了變化。
為了組合 selectors 更有效率,不妨看看 reselect。在這個例子中我們不會用到它,但它適合更大的應(yīng)用。
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);
到此為止,迷你型的任務(wù)管理應(yīng)用就開發(fā)完畢。
參照 本示例完整 來深化理解。然后就可以跳到 高級教程 學(xué)習(xí)網(wǎng)絡(luò)請求處理和路由。
更多建議: