我們研發開源了一款基於 Git 進行技術實戰教程寫作的工具,我們圖雀社群的所有教程都是用這款工具寫作而成,歡迎 Star 哦
如果你想快速瞭解如何使用,歡迎閱讀我們的 教程文件哦
在這一部分中,我們將趁熱打鐵,運用上篇教程學到的 Redux 三大核心概念來將待辦事項的剩下部分重構完成,它涉及到將 TodoList 和 Footer 部分的相關程式碼重構到 Redux,並使用 Redux combineReducers API 進行邏輯拆分和組合,使得我們可以在使用 Redux 便利的同時,又不至於讓應用的邏輯看起來臃腫不堪,複用 React 元件化的便利,我們可以讓狀態的處理也 “元件化”。
重構程式碼:將 TodoList 部分遷移到 Redux
歡迎閱讀 Redux 包教包會系列:
- Redux 包教包會(一):解救 React 狀態危機
- Redux 包教包會(二):趁熱打鐵,完全重構(也就是這篇)
- Redux 包教包會(三):各司其職,重拾初心
此教程屬於React 前端工程師學習路線的一部分,點選可檢視全部內容。
在之前的幾個小節中,我們已經把 Redux 的核心概念講完了,並且運用這些概念重構了一部分待辦事項應用,在這一小節中,我們將趁熱打鐵,完整地運用之前學到的知識,繼續用 Redux 重構我們的應用。
此時如果你在瀏覽器裡面嘗試這個待辦事項小應用,你會發現它還只可以新增新的待辦事項,對於 “完成和重做待辦事項” 以及 “過濾檢視待辦事項” 這兩個功能,目前我們還沒有使用 Redux 實現。所以當你點選單個待辦事項時,瀏覽器會報錯;當你點選底部的三個過濾器按鈕時,瀏覽器不會有任何反應。
在這一小節中,我們將使用 Redux 重構 “完成和重做待辦事項” 功能,即你可以通過點選某個待辦事項來完成它。
我們將運用 Redux 最佳實踐的開發方式來重構這一功能:
- 定義 Action Creators
- 定義 Reducers
connect
元件以及在元件中dispatch
Action
以後在開發 Redux 應用的時候,都可以使用這三步流程來周而復始地開發新的功能,或改進現有的功能。
定義 Action Creators
首先我們要定義 “完成待辦事項” 這一功能所涉及的 Action,開啟 src/actions/index.js
,修改內容內容如下:
let nextTodoId = 0;
export const addTodo = text => ({
type: "ADD_TODO",
id: nextTodoId++,
text
});
export const toggleTodo = id => ({
type: "TOGGLE_TODO",
id
});
可以看到,我們定義並匯出了一個 toggleTodo
箭頭函式,它接收 id
並返回一個型別為 "TOGGLE_TODO"
的 Action。
定義 Reducers
接著我們來定義響應 dispatch(action)
的 Reducers,開啟 src/index.js
,修改 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";
const initialState = {
todos: [
{
id: 1,
text: "你好, 圖雀",
completed: false
},
{
id: 2,
text: "我是一隻小小小小圖雀",
completed: false
},
{
id: 3,
text: "小若燕雀,亦可一展巨集圖!",
completed: false
}
],
filter: VisibilityFilters.SHOW_ALL
};
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;
}
};
const store = createStore(rootReducer, initialState);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
可以看到,我們在 switch
語句裡面新增了一個 "TOGGLE_TODO"
的判斷,並根據 action.id
來判斷對應操作的 todo,取反它目前的 completed
屬性,用來表示從完成到未完成,或從未完成到完成的操作。
connect 和 dispatch(action)
當定義了 Action,宣告瞭響應 Action 的 Reducers 之後,我們開始定義 React 和 Redux 交流的介面:connect
和 dispatch
,前者負責將 Redux Store 的內容整合進 React,後者負責從 React 中發出操作 Redux Store 的指令。
我們開啟 src/components/TodoList.js
檔案,對檔案內容作出如下的修改:
import React from "react";
import PropTypes from "prop-types";
import Todo from "./Todo";
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
修改內容如下:
import React from "react";
import AddTodo from "./AddTodo";
import TodoList from "./TodoList";
import Footer from "./Footer";
import { connect } from "react-redux";
export const VisibilityFilters = {
SHOW_ALL: "SHOW_ALL",
SHOW_COMPLETED: "SHOW_COMPLETED",
SHOW_ACTIVE: "SHOW_ACTIVE"
};
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);
}
};
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>
);
}
}
const mapStateToProps = (state, props) => ({
todos: state.todos,
filter: state.filter
});
export default connect(mapStateToProps)(App);
可以看到,我們刪除了 toggleTodo
方法,並對應刪除了定義在 constructor
中的 toggleTodo
定義以及在 render
方法中,傳給 TodoList
的 toggleTodo
屬性。
儲存上述修改的程式碼,開啟瀏覽器,你應該又可以點選單個待辦事項來完成和重做它了:
小結
在本節中,我們介紹了開發 Redux 應用的最佳實踐,並通過重構 “完成和重做待辦事項“ 這一功能來詳細實踐了這一最佳實踐。
重構程式碼:將 Footer 部分遷移到 Redux
這一節中,我們將繼續重構剩下的部分。我們將繼續遵循上一節提到的 Redux 開發的最佳實踐:
- 定義 Action Creators
- 定義 Reducers
connect
元件以及在元件中dispatch
Action
定義 Action Creators
開啟 src/actions/index.js
檔案,修改內容如下:
let nextTodoId = 0;
export const addTodo = text => ({
type: "ADD_TODO",
id: nextTodoId++,
text
});
export const toggleTodo = id => ({
type: "TOGGLE_TODO",
id
});
export const setVisibilityFilter = filter => ({
type: "SET_VISIBILITY_FILTER",
filter
});
可以看到我們建立了一個名為 setVisibilityFilter
的 Action Creators,它接收 filter
引數,然後返回一個型別為 "SET_VISIBILITY_FILTER"
的 Action。
定義 Reducers
開啟 src/index.js
檔案,修改程式碼如下:
import React from "react";
import ReactDOM from "react-dom";
import App, { VisibilityFilters } from "./components/App";
import { createStore } from "redux";
import { Provider } from "react-redux";
const initialState = {
todos: [
{
id: 1,
text: "你好, 圖雀",
completed: false
},
{
id: 2,
text: "我是一隻小小小小圖雀",
completed: false
},
{
id: 3,
text: "小若燕雀,亦可一展巨集圖!",
completed: false
}
],
filter: VisibilityFilters.SHOW_ALL
};
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
)
};
}
case "SET_VISIBILITY_FILTER": {
return {
...state,
filter: action.filter
};
}
default:
return state;
}
};
const store = createStore(rootReducer, initialState);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
可以看到,我們增加了一條 case
語句,來響應 "SET_VISIBILITY_FILTER"
Action,通過接收新的 filter
來更新 Store 中的狀態。
connect 和 dispatch(action)
開啟 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
修改內容如下:
import React from "react";
import AddTodo from "./AddTodo";
import TodoList from "./TodoList";
import Footer from "./Footer";
import { connect } from "react-redux";
export const VisibilityFilters = {
SHOW_ALL: "SHOW_ALL",
SHOW_COMPLETED: "SHOW_COMPLETED",
SHOW_ACTIVE: "SHOW_ACTIVE"
};
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);
}
};
class App extends React.Component {
render() {
const { todos, filter } = this.props;
return (
<div>
<AddTodo />
<TodoList todos={getVisibleTodos(todos, filter)} />
<Footer filter={filter} />
</div>
);
}
}
const mapStateToProps = (state, props) => ({
todos: state.todos,
filter: state.filter
});
export default connect(mapStateToProps)(App);
可以看到,我們刪除了 setVisibilityFilter
方法,並對應刪除了定義在 constructor
中的 setVisibilityFilter
定義以及在 render
方法中,傳給 Footer
的 setVisibilityFilter
屬性。
因為 constructor
方法中已經不需要再定義內容了,所以我們刪掉了它。
儲存上述修改的程式碼,開啟瀏覽器,你應該又可以繼續點選底部的按鈕來過濾完成和未完成的待辦事項了:
小結
在本節中,我們介紹了開發 Redux 應用的最佳實踐,並通過重構 “過濾檢視待辦事項“ 這一功能來詳細實踐了這一最佳實踐。
自此,我們已經使用 Redux 重構了整個待辦事項小應用,但是重構完的這份程式碼還顯得有點亂,不同型別的元件狀態混在一起。當我們的應用逐漸變得複雜時,我們的 rootReducer
就會變得非常冗長,所以是時候考慮拆分不同元件的狀態了。
我們將在下一節中講解如何將不同元件的狀態進行拆分,以確保我們在編寫大型應用時也可以顯得很從容。
combineReducers:組合拆分狀態的 Reducers
當應用邏輯逐漸複雜的時候,我們就要考慮將巨大的 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。
編寫 Reducer:todos
在 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
語句裡,進行資料改變時,要對陣列進行操作,並最後返回一個新的陣列。
編寫 Reducer:filter
我們前面使用 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
。
組合多個 Reducer
當我們將 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,它主要解決兩個問題:
- 當應用逐漸複雜的時候,我們需要對 Reducer 進行拆分,那麼我們就需要把拆分後的 Reducer 進行組合,併合並所有的 State。
- 對於每個 React 元件 dispatch 的 Action,將其分發給對應的 Reducer。
當有了 combineReducers
之後,不管我們的應用如何複雜,我們都可以將處理應用狀態的邏輯拆分都一個一個很簡潔、易懂的小檔案,然後組合這些小檔案來完成複雜的應用邏輯,這和 React 元件的組合思想類似,可以想見,元件式程式設計的威力是多麼巨大!
此教程屬於React 前端工程師學習路線的一部分,點選可檢視全部內容。
想要學習更多精彩的實戰技術教程?來圖雀社群逛逛吧。
本作品採用《CC 協議》,轉載必須註明作者和本文連結