Redux 包教包會(三):各司其職,重拾初心
在這一部分中,我們將會講解如何用 combineReducers
重構完剩下的內容,接著我們會提出 “容器元件” 和 “展示元件” 的概念,“容器元件” 用於接管 “狀態”,“展示元件” 用於渲染介面,其中 “展示元件” 也是 React 誕生的初心,專注於高效的編寫使用者介面。
重構程式碼:將 TodoList 的狀態和渲染分離
歡迎閱讀 Redux 包教包會系列:
- Redux 包教包會(三):各司其職,重拾初心(也就是這篇)
此教程屬於的一部分,點選可檢視全部內容。
展示元件和容器元件
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 如下:
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
});
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
修改導包路徑:
import React from "react";
import Link from "./Link";
import { VisibilityFilters } from "../actions";
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);
開啟 src/reducers/filter.js
修改導包路徑:
import { VisibilityFilters } from "../actions";
const filter = (state = VisibilityFilters.SHOW_ALL, action) => {
switch (action.type) {
case "SET_VISIBILITY_FILTER":
return action.filter;
default:
return state;
}
};
export default filter;
因為我們在 src/actions/index.js
中的 nextTodoId
是從 0 開始自增的,所以之前我們定義的 initialTodoState
會出現一些問題,比如新新增的 todo 的 id 會與初始的重疊,導致出現問題,所以我們刪除 src/reducers/todos.js
中對應的 initialTodoState
,然後給 todos
reducer 的 state 賦予一個 []
的預設值。
const todos = (state = [], 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;
小結
儲存修改的內容,你會發現我們的待辦事項小應用依然可以完整的執行,但是我們已經成功的將原來的 TodoList
分離成了容器元件的 VisibleTodoList
以及展示元件的 TodoList
了。
重構程式碼:將 Footer 的狀態和渲染分離
我們趁熱打鐵,用上一節學到的知識來馬上將 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 的狀態和渲染分離
讓我們來完成最後一點收尾工作,將 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:
- Store 用來儲存整個應用的狀態,這個狀態是一個被稱之為 State 的 JavaScript 物件。所有應用的狀態都是從 Store 中獲取,所以狀態的改變都是改變 Store 中的狀態,所以 Store 也有著 “資料的唯一真相來源” 的稱號。
- Action 是 Redux 中用來改變 Store 狀態的唯一手段,所有狀態的改變都是以類似
{ type: 'ACTION_TYPE', data1, data2 }
這樣的形式宣告式的定義一個 Action,然後透過dispatch
這個 Action 來發生的。 - Reducers 是用來響應 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 的使用,感謝你的閱讀!
One More Thing!
細心的讀者可能發現了,我們畫的 Redux 狀態迴圈圖都是單向的,它有一個明確的箭頭指向,這其實也是 Redux 的哲學,即 ”單向資料流“,也是 React 社群推崇的設計模式,再加上 Reducer 的純函式約定,這使得我們整個應用的每一次狀態更改都是可以被記錄下來,並且可以重現出來,或者說狀態是可預測的,它可以追根溯源的找到某一次狀態的改變時由某一個 Action 發起的,所以 Redux 也被冠名為 ”可預測的狀態管理容器“。
此教程屬於的一部分,點選可檢視全部內容。
想要學習更多精彩的實戰技術教程?來逛逛吧。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4692/viewspace-2824671/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 包教包會ReduxRedux
- Redux 包教包會(一):介紹 Redux 三大核心概念Redux
- 讓理財與保險各司其職HLX
- Redux 包教包會(二):引入 combineReducers 拆分和組合狀態邏輯Redux
- Nginx-包教包會-入門Nginx
- 不懂聖盃佈局?5種方式包教包會
- AI在用 | 搞錢!免費AI開整治癒短影片,包教包會!AI
- imtoken錢包是什麼?imtoken錢包教程
- 切圖仔最後的倔強:包教不包會設計模式 - 結構型設計模式
- Redux其實很簡單(原理篇)Redux
- 包教包會!7段程式碼帶你玩轉Python條件語句(附程式碼)Python
- 天星金融錢包保持初心繼續前行
- 蔡康永揭露職場真相:所有的工作,其實你都不會喜歡
- 2萬字長文包教包會 JVM 記憶體結構 保姆級學習筆記JVM記憶體筆記
- 各專業各類資質所需的職稱證書,兼職不坐班性質
- 重拾JSXJS
- Hibernate各個jar包作用JAR
- 【兼職】承接各類 PHP 專案PHP
- 工良出品:包教會,Hadoop、Hive 搭建部署簡易教程HadoopHive
- python mesa包教程Python
- 重拾React: ContextReactContext
- 重拾 Webpack(上卷)Web
- 快速重拾 TmuxUX
- 重拾java - jdkJavaJDK
- 淺談 React、Flux 與 Redux(各個的執行機制)ReactRedux
- 想知道jsonp到底是怎麼實現的?看我,包教會!JSON
- 【不忘初心】WindowsWindows
- 🔥《吐血整理》進階系列教程 - 拿捏 Fiddler 抓包教程 (11)-Fiddler 設定安卓手機抓包,不會可是萬萬不行的!安卓
- 孫宇晨出席Huobi品牌升級釋出會 提三大戰略助其重返三大
- 關於各種揹包問題
- 用各種方法解01揹包
- 重拾React: React 16.0React
- 重拾-Spring-AOPSpring
- 重拾 Webpack(中卷)Web
- 重拾 CSS 之 BFCCSS
- 淺談 SpringMVC 中各層職責的設計SpringMVC
- Flutter 會不會被蘋果限制其發展?Flutter蘋果
- Redux中介軟體對閉包的一個巧妙使用Redux