在這一部分中,咱們會提出 「容器組件」 和 「展現組件」 的概念,「容器組件」 用於接管 「狀態」,「展現組件」 用於渲染界面,其中 「展現組件」 也是 React 誕生的初心,專一於高效的編寫用戶界面。前端
若是您以爲咱們寫得還不錯,記得 點贊 + 關注 + 評論 三連,鼓勵咱們寫出更好的教程💪
歡迎閱讀 Redux 包教包會系列:node
此教程屬於 React 前端工程師學習路線的一部分,點擊可查看所有內容。
Redux 的出現,經過將 State 從 React 組件剝離,並將其保存在 Store 裏面,來確保狀態來源的可預測性,你可能以爲這樣就已經很好了,可是 Redux 的動做還沒完,它又進一步提出了展現組件(Presentational Components)和容器組件(Container Components)的概念,將純展現性的 React 組件和狀態進一步抽離。react
當咱們把 Redux 狀態循環圖中的 View 層進一步拆分時,它看起來是這樣的:git
即咱們在最終渲染界面的組件和 Store 中存儲的 State 之間又加了一層,咱們稱這一層爲它專門負責接收來自 Store 的 State,並把組件中想要發起的狀態改變組裝成 Action,而後經過 dispatch
函數發出。github
將狀態完全剝離以後剩下的那層稱之爲展現組件,它專門接收來自容器組件的數據,而後將其渲染成 UI 界面,並在須要改變狀態時,告知容器組件,讓其代爲 dispatch
Action。redux
首先,咱們將 App.js 中的 VisibilityFilters
移動到了 src/actions/index.js 中。由於 VisibilityFilters
定義了過濾展現 TodoList 的三種操做,和 Action 的含義更相近一點,因此咱們將類似的東西放在了一塊兒。修改 src/actions/index.js 以下:segmentfault
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);
能夠看到咱們作了這麼幾件事:
src/actions/index.js
中mapStateToProps
中獲取 todos
的操做,由於咱們已經在 VisibleTodoList 中獲取了。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 組件的狀態和渲染抽離。
咱們在 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 也被冠名爲 」可預測的狀態管理容器「。
此教程屬於 React 前端工程師學習路線的一部分,點擊可查看所有內容。想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。