在這一部分中,咱們將趁熱打鐵,運用上篇教程學到的 Redux 三大核心概念來將待辦事項的剩下部分重構完成,它涉及到將 TodoList 和 Footer 部分的相關代碼重構到 Redux,並使用 Redux combineReducers
API 進行邏輯拆分和組合,使得咱們能夠在使用 Redux 便利的同時,又不至於讓應用的邏輯看起來臃腫不堪,複用 React 組件化的便利,咱們可讓狀態的處理也 「組件化」。最後,咱們將讓 React 迴歸初心——專一於展示用戶界面,經過「容器組件」和「展現組件」將邏輯和狀態進一步分離。前端
歡迎閱讀 Redux 包教包會系列:node
此教程屬於React 前端工程師學習路線的一部分,歡迎來 Star 一波,鼓勵咱們繼續創做出更好的教程,持續更新中~。react
在上一篇教程中,咱們已經把 Redux 的核心概念講完了,而且運用這些概念重構了一部分待辦事項應用,在這一小節中,咱們將完整地運用以前學到的知識,繼續用 Redux 重構咱們的應用。若是你沒有讀過上篇教程,想直接從這一步開始,那麼請運行如下命令:git
git clone https://github.com/pftom/redux-quickstart-tutorial.git
cd redux-quickstart-tutorial
git checkout second-part
npm install && npm start
複製代碼
此時若是你在瀏覽器裏面嘗試查看這個待辦事項小應用,你會發現它還只能夠添加新的待辦事項,對於 「完成和重作待辦事項」 以及 「過濾查看待辦事項」 這兩個功能,目前咱們尚未使用 Redux 實現。因此當你點擊單個待辦事項時,瀏覽器會報錯;當你點擊底部的三個過濾器按鈕時,瀏覽器不會有任何反應。github
在這一小節中,咱們將使用 Redux 重構 「完成和重作待辦事項」 功能,即你能夠經過點擊某個待辦事項來完成它。算法
咱們將運用 Redux 最佳實踐的開發方式來重構這一功能:npm
connect
組件以及在組件中 dispatch
Action之後在開發 Redux 應用的時候,均可以使用這三步流程來周而復始地開發新的功能,或改進現有的功能。編程
首先咱們要定義 「完成待辦事項」 這一功能所涉及的 Action,打開 src/actions/index.js
,在最後面添加 toggleTodo
:redux
// 省略 nextTodoId 和 addTodo ...
export const toggleTodo = id => ({
type: "TOGGLE_TODO",
id
});
複製代碼
能夠看到,咱們定義並導出了一個 toggleTodo
箭頭函數,它接收 id
並返回一個類型爲 "TOGGLE_TODO"
的 Action。設計模式
接着咱們來定義響應 dispatch(action)
的 Reducers,打開 src/index.js
,修改 rootReducer
函數以下:
// ...
const rootReducer = (state, action) => {
switch (action.type) {
case "ADD_TODO": {
const { todos } = state;
return {
...state,
todos: [
...todos,
{
id: action.id,
text: action.text,
completed: false
}
]
};
}
case "TOGGLE_TODO": {
const { todos } = state;
return {
...state,
todos: todos.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
)
};
}
default:
return state;
}
};
// ...
複製代碼
能夠看到,咱們在 switch
語句裏面添加了一個 "TOGGLE_TODO"
的判斷,並根據 action.id
來判斷對應操做的 todo,取反它目前的 completed
屬性,用來表示從完成到未完成,或從未完成到完成的操做。
當定義了 Action,聲明瞭響應 Action 的 Reducers 以後,咱們開始定義 React 和 Redux 交流的接口:connect
和 dispatch
,前者負責將 Redux Store 的內容整合進 React,後者負責從 React 中發出操做 Redux Store 的指令。
咱們打開 src/components/TodoList.js
文件,對文件內容做出以下的修改:
// 省略沒有變化的 import 語句 ...
import { connect } from "react-redux";
import { toggleTodo } from "../actions";
const TodoList = ({ todos, dispatch }) => (
<ul> {todos.map(todo => ( <Todo key={todo.id} {...todo} onClick={() => dispatch(toggleTodo(todo.id))} /> ))} </ul> ); TodoList.propTypes = { todos: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired }).isRequired ).isRequired }; export default connect()(TodoList); 複製代碼
能夠看到,咱們對文件作出瞭如下幾步修改:
react-redux
中導出 connect
函數,它負責給 TodoList
傳入 dispatch
函數,使得咱們能夠在 TodoList
組件中 dispatch
Action。toggleTodo
Action Creators,並將以前從父組件接收 toggleTodo
方法並調用的方式改爲了當 Todo 被點擊以後,咱們 dispatch(toggle(todo.id))
。propsTypes
中再也不須要的 toggleTodo
。當咱們經過以上三步整合了 Redux 的內容以後,咱們就能夠刪除原 App.js
中沒必要要的代碼了,修改後的 src/components/App.js
內容以下:
// ...
class App extends React.Component {
constructor(props) {
super(props);
this.setVisibilityFilter = this.setVisibilityFilter.bind(this);
}
setVisibilityFilter(filter) {
this.setState({
filter: filter
});
}
render() {
const { todos, filter } = this.props;
return (
<div>
<AddTodo />
<TodoList todos={getVisibleTodos(todos, filter)} />
<Footer
filter={filter}
setVisibilityFilter={this.setVisibilityFilter}
/>
</div>
);
}
}
// ...
複製代碼
能夠看到,咱們刪除了 toggleTodo
方法,並對應刪除了定義在 constructor
中的 toggleTodo
定義以及在 render
方法中,傳給 TodoList
的 toggleTodo
屬性。
保存上述修改的代碼,打開瀏覽器,你應該又能夠點擊單個待辦事項來完成和重作它了:
在本節中,咱們介紹了開發 Redux 應用的最佳實踐,並經過重構 "完成和重作待辦事項「 這一功能來詳細實踐了這一最佳實踐。
這一節中,咱們將繼續重構剩下的部分。咱們將繼續遵循上一節提到的 Redux 開發的最佳實踐:
connect
組件以及在組件中 dispatch
Action打開 src/actions/index.js
文件,在最後面添加 setVisibilityFilter
:
// ...
export const setVisibilityFilter = filter => ({
type: "SET_VISIBILITY_FILTER",
filter
});
複製代碼
能夠看到咱們建立了一個名爲 setVisibilityFilter
的 Action Creators,它接收 filter
參數,而後返回一個類型爲 "SET_VISIBILITY_FILTER"
的 Action。
打開 src/index.js
文件,修改 rootReducer
以下:
// ...
const rootReducer = (state, action) => {
switch (action.type) {
// 省略處理 ADD_TODO 和 TOGGLE_TODO 的 reducers ...
case "SET_VISIBILITY_FILTER": {
return {
...state,
filter: action.filter
};
}
default:
return state;
}
};
// ...
複製代碼
能夠看到,咱們增長了一條 case
語句,來響應 "SET_VISIBILITY_FILTER"
Action,經過接收新的 filter
來更新 Store 中的狀態。
打開 src/components/Footer.js
文件,修改代碼以下:
import React from "react";
import Link from "./Link";
import { VisibilityFilters } from "./App";
import { connect } from "react-redux";
import { setVisibilityFilter } from "../actions";
const Footer = ({ filter, dispatch }) => (
<div> <span>Show: </span> <Link active={VisibilityFilters.SHOW_ALL === filter} onClick={() => dispatch(setVisibilityFilter(VisibilityFilters.SHOW_ALL))} > All </Link> <Link active={VisibilityFilters.SHOW_ACTIVE === filter} onClick={() => dispatch(setVisibilityFilter(VisibilityFilters.SHOW_ACTIVE)) } > Active </Link> <Link active={VisibilityFilters.SHOW_COMPLETED === filter} onClick={() => dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED)) } > Completed </Link> </div>
);
export default connect()(Footer);
複製代碼
能夠看到,上面的文件主要作了這幾件事:
react-redux
中導出 connect
函數,它負責給 Footer
傳入 dispatch
函數,使得咱們能夠在 Footer
組件中 dispatch
Action。setVisibilityFilter
Action Creators,並將以前從父組件接收 setVisibilityFilter
方法並調用的方式改爲了當 Link 被點擊以後,咱們 dispatch
對應的 Action 。當咱們經過以上三步整合了 Redux 的內容以後,咱們就能夠刪除原 App.js
中沒必要要的代碼了,打開 src/components/App.js
修改內容以下:
// ...
class App extends React.Component {
render() {
const { todos, filter } = this.props;
return (
<div>
<AddTodo />
<TodoList todos={getVisibleTodos(todos, filter)} />
<Footer filter={filter} />
</div>
);
}
}
// ...
複製代碼
能夠看到,咱們刪除了 setVisibilityFilter
方法,並對應刪除了定義在 constructor
中的 setVisibilityFilter
定義以及在 render
方法中,傳給 Footer
的 setVisibilityFilter
屬性。
由於 constructor
方法中已經不須要再定義內容了,因此咱們刪掉了它。
保存上述修改的代碼,打開瀏覽器,你應該又能夠繼續點擊底部的按鈕來過濾完成和未完成的待辦事項了:
在本節中,咱們介紹了開發 Redux 應用的最佳實踐,並經過重構 "過濾查看待辦事項「 這一功能來詳細實踐了這一最佳實踐。
自此,咱們已經使用 Redux 重構了整個待辦事項小應用,可是重構完的這份代碼還顯得有點亂,不一樣類型的組件狀態混在一塊兒。當咱們的應用逐漸變得複雜時,咱們的 rootReducer
就會變得很是冗長,因此是時候考慮拆分不一樣組件的狀態了。
咱們將在下一節中講解如何將不一樣組件的狀態進行拆分,以確保咱們在編寫大型應用時也能夠顯得很從容。
當應用邏輯逐漸複雜的時候,咱們就要考慮將巨大的 Reducer 函數拆分紅一個個獨立的單元,這在算法中被稱爲 」分而治之「。
Reducers 在 Redux 中其實是用來處理 Store 中存儲的 State 中的某個部分,一個 Reducer 和 State 對象樹中的某個屬性一一對應,一個 Reducer 負責處理 State 中對應的那個屬性。好比咱們來看一下如今咱們的 State 的結構:
const initialState = {
todos: [
{
id: 1,
text: "你好, 圖雀",
completed: false
},
{
id: 2,
text: "我是一隻小小小小圖雀",
completed: false
},
{
id: 3,
text: "小若燕雀,亦可一展宏圖!",
completed: false
}
],
filter: VisibilityFilters.SHOW_ALL
};
複製代碼
由於 Reducer 對應着 State 相關的部分,這裏咱們的 State 有兩個部分:todos
和 filter
,因此咱們能夠編寫兩個對應的 Reducer。
在 Redux 最佳實踐中,由於 Reducer 對應修改 State 中的相關部分,當 State 對象樹很大時,咱們的 Reducer 也會有不少,因此咱們通常會單獨建一個 reducers
文件夾來存放這些 "reducers「。
咱們在 src
目錄下新建 reducers
文件夾,而後在裏面新建一個 todos.js
文件,表示處理 State 中對應 todos
屬性的 Reducer,代碼以下:
const initialTodoState = [
{
id: 1,
text: "你好, 圖雀",
completed: false
},
{
id: 2,
text: "我是一隻小小小小圖雀",
completed: false
},
{
id: 3,
text: "小若燕雀,亦可一展宏圖!",
completed: false
}
];
const todos = (state = initialTodoState, action) => {
switch (action.type) {
case "ADD_TODO": {
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
}
case "TOGGLE_TODO": {
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
}
default:
return state;
}
};
export default todos;
複製代碼
能夠看到,上面的代碼作了這幾件事:
initialState
裏面的 todos
部分拆分到了 src/reducers/todos.js
文件裏,咱們定義了一個 initialTodoState
表明以前的 initialState
的 todos
部分,它是一個數組,並把它賦值給 todos
函數中 state
參數的默認值,即當調用此函數時,若是傳進來的 state 參數爲 undefined
或者 null
時,這個 state
就是 initialState
。todos
箭頭函數,它的結構和 rootReducer
相似,都是接收兩個參數:state
和 action
,而後進入一個 switch
判斷語句,根據 action.type
判斷要相應的 Action 類型,而後對 state
執行對應的操做。注意
咱們的
todos
reducers 只負責處理原initialState
的todos
部分,因此這裏它的state
就是原todos
屬性,它是一個數組,因此咱們在switch
語句裏,進行數據改變時,要對數組進行操做,並最後返回一個新的數組。
咱們前面使用 todos
reducer 解決了原 initialState
的 todos
屬性操做問題,如今咱們立刻來說解剩下的 filter
屬性的操做問題。
在 src/reducers
文件夾下建立 filter.js
文件,在其中加入以下的內容:
import { VisibilityFilters } from "../components/App";
const filter = (state = VisibilityFilters.SHOW_ALL, action) => {
switch (action.type) {
case "SET_VISIBILITY_FILTER":
return action.filter;
default:
return state;
}
};
export default filter;
複製代碼
能夠看到咱們定義了一個 filter
箭頭函數,它接收兩個參數:state
和 action
,由於這個 filter
reducer 只負責處理原 initialState
的 filter
屬性部分,因此這裏這個 state
參數就是原 filter
屬性,這裏咱們給了它一個默認值。
注意
filter 函數的剩餘部分和
rootReducer
相似,可是注意這裏它的state
是對filter
屬性進行操做,因此當判斷"SET_VISIBILITY_FILTER"
action 類型時,它只是單純的返回action.filter
。
當咱們將 rootReducer
的邏輯拆分,並對應處理 Store 中保存的 State 中的屬性以後,咱們能夠確保每一個 reducer 都很小,這個時候咱們就要考慮如何將這些小的 reducer 組合起來,構成最終的 rootReducer,這種組合就像咱們組合 React 組件同樣,最終只有一個根級組件,在咱們的待辦事項小應用裏面,這個組件就是 App.js
組件。
Redux 爲咱們提供了 combineReducers
API,用來組合多個小的 reducer,咱們在 src/reducers
文件夾下建立 index.js
文件,並在裏面添加以下內容:
import { combineReducers } from "redux";
import todos from "./todos";
import filter from "./filter";
export default combineReducers({
todos,
filter
});
複製代碼
能夠看到,咱們從 redux
模塊中導出了 combineReducers
函數,而後導出了以前定義的 todos
和 filter
reducer。
接着咱們經過對象簡潔表示法,將 todos
和 filter
做爲對象屬性合在一塊兒,而後傳遞給 combineReducers
函數,這裏 combineReducers
內部就會對 todos
和 filter
進行操做,而後生成相似咱們以前的 rootReducer
形式。最後咱們導出生成的 rootReducer
。
combineReducers 主要有兩個做用:
1)組合全部 reducer 的 state,最後組合成相似咱們以前定義的
initialState
對象狀態樹。即這裏
todos
reducer 的 state 爲:state = [ { id: 1, text: "你好, 圖雀", completed: false }, { id: 2, text: "我是一隻小小小小圖雀", completed: false }, { id: 3, text: "小若燕雀,亦可一展宏圖!", completed: false } ]; 複製代碼
filter
reducer 的 state 爲:state = VisibilityFilters.SHOW_ALL 複製代碼
那麼經過
combineReducers
組合這兩個reducer
的state
獲得的最終結果爲:state = { todos: [ { id: 1, text: "你好, 圖雀", completed: false }, { id: 2, text: "我是一隻小小小小圖雀", completed: false }, { id: 3, text: "小若燕雀,亦可一展宏圖!", completed: false } ], filter: VisibilityFilters.SHOW_ALL }; 複製代碼
這個經過
combineReducers
組合後的最終state
就是存儲在 Store 裏面的那棵 State JavaScript 對象狀態樹。2)分發
dispatch
的 Action。經過
combineReducers
組合todos
和filter
reducer 以後,從 React 組件中dispatch
Action會遍歷檢查todos
和filter
reducer,判斷是否存在響應對應action.type
的case
語句,若是存在,全部的這些case
語句都會響應。
當咱們將原 rootReducer
拆分紅了 todos
和 filter
兩個 reducer ,並經過 redux
提供的 combineReducers
API 進行組合後,咱們以前在 src/index.js
定義的 initialState
和 rootReducer
就再也不須要了,刪除後整個文件的代碼以下:
import React from "react";
import ReactDOM from "react-dom";
import App, { VisibilityFilters } from "./components/App";
import { createStore } from "redux";
import { Provider } from "react-redux";
import rootReducer from "./reducers";
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}> <App /> </Provider>,
document.getElementById("root")
);
複製代碼
能夠看到,咱們從刪除了以前在 src/index.js
定義的 rootReducer
,轉而使用了從 src/reducers/index.js
導出的 rootReducer
。
而且咱們咱們以前講到,combineReducers
的第一個功能就是組合多個 reducer 的 state,最終合併成一個大的 JavaScript 對象狀態樹,而後自動存儲在 Redux Store 裏面,因此咱們再也不須要給 createStore
顯式的傳遞第二個 initialState
參數了。
保存修改的內容,打開瀏覽器,能夠照樣能夠操做全部的功能,你能夠加點待辦事項,點擊某個待辦事項以完成它,經過底部的三個過濾按鈕查看不一樣狀態下的待辦事項:
在這一小節中,咱們講解了 redux
提供的 combineReducers
API,它主要解決兩個問題:
當有了 combineReducers
以後,無論咱們的應用如何複雜,咱們均可以將處理應用狀態的邏輯拆分都一個一個很簡潔、易懂的小文件,而後組合這些小文件來完成複雜的應用邏輯,這和 React 組件的組合思想相似,能夠想見,組件式編程的威力是多麼巨大!
Redux 的出現,經過將 State 從 React 組件剝離,並將其保存在 Store 裏面,來確保狀態來源的可預測性,你可能以爲這樣就已經很好了,可是 Redux 的動做還沒完,它又進一步提出了展現組件(Presentational Components)和容器組件(Container Components)的概念,將純展現性的 React 組件和狀態進一步抽離。
當咱們把 Redux 狀態循環圖中的 View 層進一步拆分時,它看起來是這樣的:
即咱們在最終渲染界面的組件和 Store 中存儲的 State 之間又加了一層,咱們稱這一層爲它專門負責接收來自 Store 的 State,並把組件中想要發起的狀態改變組裝成 Action,而後經過 dispatch
函數發出。
將狀態完全剝離以後剩下的那層稱之爲展現組件,它專門接收來自容器組件的數據,而後將其渲染成 UI 界面,並在須要改變狀態時,告知容器組件,讓其代爲 dispatch
Action。
首先,咱們將 App.js 中的 VisibilityFilters
移到了 src/actions/index.js 的最後。由於 VisibilityFilters
定義了過濾展現 TodoList 的三種操做,和 Action 的含義更相近一點,因此咱們將類似的東西放在了一塊兒。修改 src/actions/index.js 以下:
// 省略了 nextTodoId 和以前定義的三個 Action
export const VisibilityFilters = {
SHOW_ALL: "SHOW_ALL",
SHOW_COMPLETED: "SHOW_COMPLETED",
SHOW_ACTIVE: "SHOW_ACTIVE"
};
複製代碼
容器組件其實也是一個 React 組件,它只是將原來從 Store 到 View 的狀態和從組件中 dispatch
Action 這兩個邏輯從原組件中抽離出來。
根據 Redux 的最佳實踐,容器組件通常保存在 containers
文件夾中,咱們在 src
文件夾下創建一個 containers
文件夾,而後在裏面新建 VisibleTodoList.js
文件,用來表示原 TodoList.js
的容器組件,並在文件中加入以下代碼:
import { connect } from "react-redux";
import { toggleTodo } from "../actions";
import TodoList from "../components/TodoList";
import { VisibilityFilters } from "../actions";
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos;
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(t => t.completed);
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(t => !t.completed);
default:
throw new Error("Unknown filter: " + filter);
}
};
const mapStateToProps = state => ({
todos: getVisibleTodos(state.todos, state.filter)
});
const mapDispatchToProps = dispatch => ({
toggleTodo: id => dispatch(toggleTodo(id))
});
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
複製代碼
能夠看到,上面的代碼主要作了這幾件事情:
mapStateToProps
,這是咱們以前詳細講解過,它主要是能夠獲取到來自 Redux Store 的 State 以及組件自身的原 Props,而後組合這二者成新的 Props,而後傳給組件,這個函數是 Store 到組件的惟一接口。這裏咱們將以前定義在 App.js
中的 getVisibleTodos
函數移過來,並根據 state.filter
過濾條件返回相應須要展現的 todos
。mapDispatchToProps
函數,這個函數接收兩個參數:dispatch
和 ownProps
,前者咱們很熟悉了就是用來發出更新動做的函數,後者就是原組件的 Props,它是一個可選參數,這裏咱們沒有聲明它。咱們主要在這個函數聲明式的定義全部須要 dispatch
的 Action 函數,並將其做爲 Props 傳給組件。這裏咱們定義了一個 toggleTodo
函數,使得在組件中經過調用 toggleTodo(id)
就能夠 dispatch(toggleTodo(id))
。connect
函數接收 mapStateToProps
和 mapDispatchToProps
並調用,而後再接收 TodoList 組件並調用,返回最終的導出的容器組件。當咱們編寫了 TodoList
的容器組件以後,接着咱們要考慮就是抽離了 State 和 dispatch
的關於 TodoList 的展現組件了。
修改 src/components/TodoList.js
,代碼以下:
import React from "react";
import PropTypes from "prop-types";
import Todo from "./Todo";
const TodoList = ({ todos, toggleTodo }) => (
<ul> {todos.map(todo => ( <Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} /> ))} </ul> ); TodoList.propTypes = { todos: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired }).isRequired ).isRequired, toggleTodo: PropTypes.func.isRequired }; export default TodoList; 複製代碼
在上面的代碼中,咱們刪除了 connect
和 toggleTodo
Action,並將 TodoList 接收的 dispatch
屬性刪除,轉而改爲經過 mapDispatchToProps
傳進來的 toggleTodo
函數,並在 Todo 被點擊時調用 toggleTodo
函數。
固然咱們的 toggleTodo
屬性又回來了,因此咱們在 propTypes
中恢復以前刪除的 toggleTodo
。:)
最後,咱們再也不須要 connect()(TodoList)
,由於 VisibleTodoList.js
中定義的 TodoList
的對應容器組件會取到 Redux Store 中的 State,而後傳給 TodoList。
能夠看到,TodoList
不用再考慮狀態相關的操做,只須要專心地作好界面的展現和動做的響應。咱們進一步將狀態與渲染分離,讓合適的人作 TA 最擅長的事。
由於咱們將原來的 TodoList 剝離成了容器組件和 展現組件,因此咱們要將 App.js
裏面對應的 TodoList
換成咱們的 VisibleTodoList
,由容器組件來提供原 TodoList 對外的接口。
咱們打開 src/components/App.js
對相應的內容做出以下修改:
import React from "react";
import AddTodo from "./AddTodo";
import VisibleTodoList from "../containers/VisibleTodoList";
import Footer from "./Footer";
import { connect } from "react-redux";
class App extends React.Component {
render() {
const { filter } = this.props;
return (
<div> <AddTodo /> <VisibleTodoList /> <Footer filter={filter} /> </div> ); } } const mapStateToProps = (state, props) => ({ filter: state.filter }); export default connect(mapStateToProps)(App); 複製代碼
能夠看到咱們作了這麼幾件事:
TodoList
更換成 VisibleTodoList。
VisibilityFilters,由於它已經被放到了
src/actions/index.js
中getVisibleTodos
,由於它已經被放到了 VisibleTodoList
中。mapStateToProps
中獲取 todos
的操做,由於咱們已經在 VisibleTodoList
中獲取了。App
組件中的 todos
。接着咱們處理一下因 VisibilityFilters
變更而引發的其餘幾個文件的導包問題。
打開 src/components/Footer.js
修改 VisibilityFilters
的導包路徑:
import React from "react";
import Link from "./Link";
import { VisibilityFilters } from "../actions";
// ...
複製代碼
打開 src/reducers/filter.js
修改 VisibilityFilters
的導包路徑:
import { VisibilityFilters } from "../actions";
// ...
複製代碼
由於咱們在 src/actions/index.js
中的 nextTodoId
是從 0 開始自增的,因此以前咱們定義的 initialTodoState
會出現一些問題,好比新添加的 todo 的 id 會與初始的重疊,致使出現問題,因此咱們刪除 src/reducers/todos.js
中對應的 initialTodoState
,而後給 todos
reducer 的 state 賦予一個 []
的默認值。
const todos = (state = [], action) => {
switch (action.type) {
// ...
}
};
export default todos;
複製代碼
保存修改的內容,你會發現咱們的待辦事項小應用依然能夠完整的運行,可是咱們已經成功的將原來的 TodoList
分離成了容器組件的 VisibleTodoList
以及展現組件的 TodoList
了。
咱們趁熱打鐵,用上一節學到的知識來立刻將 Footer 組件的狀態和渲染抽離。
咱們在 src/containers
文件夾下建立一個 FilterLink.js
文件,添加對應的內容以下:
import { connect } from "react-redux";
import { setVisibilityFilter } from "../actions";
import Link from "../components/Link";
const mapStateToProps = (state, ownProps) => ({
active: ownProps.filter === state.filter
});
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
});
export default connect(mapStateToProps, mapDispatchToProps)(Link);
複製代碼
能夠看到咱們作了如下幾件工做:
mapStateToProps
,它負責比較 Redux Store 中保存的 State 的 state.filter
屬性和組件接收父級傳下來的 ownProps.filter
屬性是否相同,若是相同,則把 active
設置爲 true
。mapDispatchToProps
,它經過返回一個 onClick
函數,當組件點擊時,調用生成一個 dispatch
Action,將此時組件接收父級傳下來的 ownProps.filter
參數傳進 setVisibilityFilter
,生成 action.type
爲 "SET_VISIBILITY_FILTER"
的 Action,並 dispatch
這個 Action。connect
組合這二者,將對應的屬性合併進 Link
組件並導出。咱們如今應該能夠在 Link
組件中取到咱們在上面兩個函數中定義的 active
和 onClick
屬性了。接着咱們來編寫原 Footer 的展現組件部分,打開 src/components/Footer.js
文件,對相應的內容做出以下的修改:
import React from "react";
import FilterLink from "../containers/FilterLink";
import { VisibilityFilters } from "../actions";
const Footer = () => (
<div> <span>Show: </span> <FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink> <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink> <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink> </div>
);
export default Footer;
複製代碼
能夠看到上面的代碼修改作了這麼幾件工做:
Link
換成了 FilterLink
。請注意當組件的狀態和渲染分離以後,咱們將使用容器組件爲導出給其餘組件使用的組件。FilterLink
組件,並傳遞對應的三個 FilterLink
過濾器類型。connect
和 setVisibilityFilter
導出。filter
和 dispatch
屬性,由於它們已經在 FilterLink
中定義並傳給了 Link
組件了。當咱們將 Footer 中的狀態和渲染拆分以後,src/components/App.js
對應的 Footer 相關的內容就再也不須要了,咱們對文件中對應的內容做出以下修改:
import React from "react";
import AddTodo from "./AddTodo";
import VisibleTodoList from "../containers/VisibleTodoList";
import Footer from "./Footer";
class App extends React.Component {
render() {
return (
<div> <AddTodo /> <VisibleTodoList /> <Footer /> </div>
);
}
}
export default App;
複製代碼
能夠看到咱們作了以下工做:
App
組件中對應的 filter
屬性和 mapStateToProps
函數,由於咱們已經在 FilterLink
中獲取了對應的屬性,因此咱們再也不須要直接從 App 組件傳給 Footer 組件了。connect
函數。connect(mapStateToProps)()
,由於 App 再也不須要直接從 Redux Store 中獲取內容了。保存修改的內容,你會發現咱們的待辦事項小應用依然能夠完整的運行,可是咱們已經成功的將原來的 Footer
分離成了容器組件的 FilterLink
以及展現組件的 Footer
了。
讓咱們來完成最後一點收尾工做,將 AddTodo
組件的狀態和渲染分離。
咱們在 src/containers
文件夾中建立 AddTodoContainer.js
文件,在其中添加以下內容:
import { connect } from "react-redux";
import { addTodo } from "../actions";
import AddTodo from "../components/AddTodo";
const mapStateToProps = (state, ownProps) => {
return ownProps;
};
const mapDispatchToProps = dispatch => ({
addTodo: text => dispatch(addTodo(text))
});
export default connect(mapStateToProps, mapDispatchToProps)(AddTodo);
複製代碼
能夠看到咱們作了幾件熟悉的工做:
mapStateToProps
,由於 AddTodo
不須要從 Redux Store 中取內容,因此 mapStateToProps
只是單純地填充 connect
的第一個參數,而後簡單地返回組件的原 props
,不起其它做用。mapDispatchToProps
,咱們定義了一個 addTodo
函數,它接收 text
,而後 dispatch
一個 action.type
爲 "ADD_TODO"
的 Action。connect
組合這二者,將對應的屬性合併進 AddTodo
組件並導出。咱們如今應該能夠在 AddTodo
組件中取到咱們在上面兩個函數中定義的 addTodo
屬性了。接着咱們來編寫 AddTodo 的展現組件部分,打開 src/components/AddTodo.js
文件,對相應的內容做出以下的修改:
import React from "react";
const AddTodo = ({ addTodo }) => {
let input;
return (
<div> <form onSubmit={e => { e.preventDefault(); if (!input.value.trim()) { return; } addTodo(input.value); input.value = ""; }} > <input ref={node => (input = node)} /> <button type="submit">Add Todo</button> </form> </div> ); }; export default AddTodo; 複製代碼
能夠看到,上面的代碼作了這麼幾件工做:
connect
函數,而且去掉了其對 AddTodo
的包裹。AddTodo
接收的屬性從 dispatch
替換成從 AddTodoContainer
傳過來的 addTodo
函數,當表單提交時,它將被調用,dispatch
一個 action.type
爲 "ADD_TODO"
,text
爲 input.value
的 Action。由於咱們將原 TodoList
分離成了容器組件 AddTodoContainer
和展現組件 TodoList
,因此咱們須要對 src/components/App.js
作出以下的修改:
import React from "react";
import AddTodoContainer from "../containers/AddTodoContainer";
import VisibleTodoList from "../containers/VisibleTodoList";
import Footer from "./Footer";
class App extends React.Component {
render() {
return (
<div> <AddTodoContainer /> <VisibleTodoList /> <Footer /> </div>
);
}
}
export default App;
複製代碼
能夠看到咱們使用 AddTodoContainer
替換了原來的 AddTodo
導出,並在 render
方法中渲染 AddTodoContainer
組件。
保存修改的內容,你會發現咱們的待辦事項小應用依然能夠完整的運行,可是咱們已經成功的將原來的 AddTodo
分離成了容器組件的 AddTodoContainer
以及展現組件的 AddTodo
了。
到目前爲止,咱們就已經學習完了 Redux 的全部基礎概念,而且運用這些基礎概念將一個純 React 版的待辦事項一步一步重構到了 Redux。
讓咱們最後一次祭出 Redux 狀態循環圖,回顧咱們在這篇教程中學到的知識:
咱們在這一系列教程中首先提出了 Redux 的三大概念:Store,Action,Reducers:
{ type: 'ACTION_TYPE', data1, data2 }
這樣的形式聲明式的定義一個 Action,而後經過 dispatch
這個 Action 來發生的。switch
語句匹配 action.type
,經過對 State 的屬性進行增刪改查,而後返回一個新 State 的操做。同時它也是一個純函數,即不會直接修改 State
自己。具體反映到咱們重構的待辦事項項目裏,咱們使用 Store 保存的狀態來替換以前 React 中的 this.state
,使用 Action 來代替以前 React 發起修改 this.state
的動做,經過 dispatch
Action 來發起修改 Store 中狀態的操做,使用 Reducers 代替以前 React 中更新狀態的 this.setState
操做,純化的更新 Store 裏面保存的 State。
接着咱們趁熱打鐵,使用以前學到的三大概念,將整個待辦事情的剩下部分重構到了 Redux。
可是重構完咱們發現,咱們如今的 rootReducer
函數已經有點臃腫了,它包含了 todos
和 filter
兩類不一樣的狀態屬性,而且若是咱們想要繼續擴展這個待辦事項應用,那麼還會繼續添加不一樣的狀態屬性,到時候各類狀態屬性的操做夾雜在一塊兒很容易形成混亂和下降代碼的可讀性,不利於維護,所以咱們提出了 combineReducers
方法,用於切分 rootReducer
到多個分散在不一樣文件的保存着單一狀態屬性的 Reducer,,而後經過 combineReducers
來組合這些拆分的 Reducers。
詳細講解 combineReducers
的概念以後,咱們接着將以前的不徹底重構的 Redux 代碼進行了又一次重構,將 rootReducer
拆分紅了 todos
和 filter
兩個 Reducer。
最後咱們更進一步,讓 React 重拾初心—— 專一於用戶界面的展現,讓應用的狀態和渲染分離,咱們提出了展現組件和容器組件的概念,前者是完徹底全的 React,接收來自後者的數據,而後負責將數據高效正確的渲染;前者負責響應用戶的操做,而後交給後者發出具體的指令,能夠看到,當咱們使用 Redux 以後,咱們在 React 上蓋了一層邏輯,這層邏輯徹底負責狀態方面的工做,這就是 Redux 的精妙之處啊!
但願看到這裏的同窗能對 Redux 有個很好的瞭解,並能靈活的結合 React 和 Redux 的使用,感謝你的閱讀!
細心的讀者可能發現了,咱們畫的 Redux 狀態循環圖都是單向的,它有一個明確的箭頭指向,這其實也是 Redux 的哲學,即 」單向數據流「,也是 React 社區推崇的設計模式,再加上 Reducer 的純函數約定,這使得咱們整個應用的每一次狀態更改都是能夠被記錄下來,而且能夠重現出來,或者說狀態是可預測的,它能夠追根溯源的找到某一次狀態的改變時由某一個 Action 發起的,因此 Redux 也被冠名爲 」可預測的狀態管理容器「。
想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。