教學:使用 connect
API
我們現在建議使用 React-Redux hook API 作為預設值 。但是, connect
API 仍然運作良好。
本教學也會顯示一些較舊但不再建議的做法,例如根據種類將 Redux 邏輯分到不同的資料夾。為了完整性,我們維持此教學不變,但建議閱讀 「Redux Essential 教學」 和 Redux 文件中的 Redux Style Guide,以了解目前的最佳做法。
我們正在編寫新的教學,屆時將介紹 hook API。在這之前,我們建議閱讀 Redux Fundamentals, 第 5 部分:UI 和 React 以了解 hook 教學。
為了瞭解如何在實際中使用 React Redux,我們將透過建立待辦事項清單應用程式,展示一個循序漸進的範例。
待辦事項清單範例
前往
React UI 元件
我們的 React UI 元件實作如下
TodoApp
我們的應用程式的進入元件。它會呈現標頭、AddTodo
、TodoList
和VisibilityFilters
元件。AddTodo
是讓使用者輸入待辦事項,並在按下「新增待辦事項」按鈕後將其加入清單的元件- 它使用受控輸入來設定
onChange
時的狀態。 - 當使用者按下「新增待辦事項」按鈕時,它會傳送動作(我們會使用 React Redux 來提供),將待辦事項加到儲存中。
- 它使用受控輸入來設定
TodoList
是呈現待辦事項清單的元件- 當選取其中一個
VisibilityFilters
時,它會呈現已過濾的待辦事項清單。
- 當選取其中一個
Todo
是呈現單一待辦事項的元件- 它會呈現待辦事項內容,並透過刪除線來顯示待辦事項已完成。
- 它會傳送動作來切換待辦事項的完成狀態,在
onClick
時。
VisibilityFilters
呈現簡單的一組篩選條件:全部、已完成 和 未完成。 點選其中一個會篩選待辦事項- 它接受父層的
activeFilter
屬性,用來表示使用者目前選取的篩選條件。使用底線來呈現目前的篩選條件。 - 它會傳送
setFilter
動作來更新選取的篩選條件。
- 它接受父層的
constants
中包含我們應用程式的常數資料。- 最後,
index
會將我們的應用程式呈現到 DOM 中。
Redux 儲存
這個應用程式的 Redux 部分使用 Redux 文件中建議的模式 來設定
- 儲存
todos
:一個正規化的待辦事項的簡化器。它包含一個byIds
地圖,用來儲存所有待辦事項,以及一個allIds
清單,用來儲存所有識別碼的清單。visibilityFilters
:一個簡單的字串,可能是all
、completed
或incomplete
。
- 動作產生器
addTodo
建立一個動作來新增待辦事項。它會接收一個字串變數content
,並回傳一個ADD_TODO
動作,其payload
中包含一個自動遞增的id
和content
toggleTodo
建立一個動作來切換待辦事項。它會接收一個數字變數id
,並回傳一個TOGGLE_TODO
動作,其payload
中僅包含id
setFilter
產生動作來設定應用程式中使用中的篩選功能。函數接收單一字串變數filter
,並傳回一個SET_FILTER
動作,其中payload
包含filter
功能
- 還原器
todos
還原器- 在接收到
ADD_TODO
動作時,將id
附加到它的allIds
欄位,並在byIds
欄位中設定 to-do - 在接收到
TOGGLE_TODO
動作時,切換 to-do 的completed
欄位
- 在接收到
visibilityFilters
還原器將其儲存單位設定為它從SET_FILTER
動作有效載荷接收到的新的篩選器
- 動作類型
- 我們使用檔案
actionTypes.js
來儲存動作類型常數,俾供重複使用
- 我們使用檔案
- 選擇器
getTodoList
從todos
儲存單位傳回allIds
清單getTodoById
在儲存單位中找到指定為id
的 to-dogetTodos
稍微複雜一點。函數會從allIds
中提取所有id
,在byIds
中找到每個 to-do,並傳回 to-do 的最後陣列getTodosByVisibilityFilter
根據可視化篩選器條件篩選 to-do
你可以在 這個 CodeSandbox 查看上述使用者介面元件和未連接 Redux 儲存單位的原始程式碼。
接下來,我們將示範如何使用 React Redux 將這個儲存單位連線到我們應用程式。
提供儲存單位
首先,我們要讓應用程式可以使用 store
。為執行這項動作,我們使用 React Redux 提供的 <Provider />
API 函數包裝我們的應用程式。
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import TodoApp from './TodoApp'
import { Provider } from 'react-redux'
import store from './redux/store'
// As of React 18
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<TodoApp />
</Provider>,
)
請注意,我們的 <TodoApp />
現在以 <Provider />
包裝,並在 store
中傳遞 prop。
連接元件
React Redux 提供 connect
函數,你可用這個函數從 Redux 儲存單位讀取值(而且要在儲存單位更新時重新讀取值)。
connect
函數包含兩個引數,兩個都是選項
mapStateToProps
:這個函數會在儲存狀態每次變更時呼叫。它會接收到完整的儲存狀態,並應傳回這個元件所需的資料物件。mapDispatchToProps
:這個參數可以是函數,也可以是物件。- 如果是函數,這個函數會在元件產生時呼叫一次。函數會將
dispatch
作為引數接收,並應傳回一個包含許多函數的物件,這些函數會使用dispatch
來調度動作。 - 如果是包含許多動作產生器的物件,每個動作產生器都會轉為 prop 函數,在呼叫時會自動傳送動作。注意:我們建議使用這個「物件簡寫」格式。
- 如果是函數,這個函數會在元件產生時呼叫一次。函數會將
一般而言,你會採用下列方式呼叫 connect
const mapStateToProps = (state, ownProps) => ({
// ... computed data from state and optionally ownProps
})
const mapDispatchToProps = {
// ... normally is an object full of action creators
}
// `connect` returns a new function that accepts the component to wrap:
const connectToStore = connect(mapStateToProps, mapDispatchToProps)
// and that function returns the connected, wrapper component:
const ConnectedComponent = connectToStore(Component)
// We normally do both in one step, like this:
connect(mapStateToProps, mapDispatchToProps)(Component)
首先來處理 <AddTodo />
。它需要觸發 store
的變更,才能新增新的待辦事項。因此,它需要有能力 dispatch
action 到 store。以下是如何執行。
我們的 addTodo
action 建立器看起來像這樣
// redux/actions.js
import { ADD_TODO } from './actionTypes'
let nextTodoId = 0
export const addTodo = (content) => ({
type: ADD_TODO,
payload: {
id: ++nextTodoId,
content,
},
})
// ... other actions
將它傳遞給 connect
後,我們的元件會接收它作為 prop,並在呼叫時自動 dispatch 該 action。
// components/AddTodo.js
// ... other imports
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'
class AddTodo extends React.Component {
// ... component implementation
}
export default connect(null, { addTodo })(AddTodo)
現在請注意,<AddTodo />
會封裝一個名為 <Connect(AddTodo) />
的父元件。同時,<AddTodo />
現在獲得了一個 prop:addTodo
action。
我們也需要實作 handleAddTodo
function,以讓它 dispatch addTodo
action 並重設輸入
// components/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'
class AddTodo extends React.Component {
// ...
handleAddTodo = () => {
// dispatches actions to add todo
this.props.addTodo(this.state.input)
// sets state back to empty string
this.setState({ input: '' })
}
render() {
return (
<div>
<input
onChange={(e) => this.updateInput(e.target.value)}
value={this.state.input}
/>
<button className="add-todo" onClick={this.handleAddTodo}>
Add Todo
</button>
</div>
)
}
}
export default connect(null, { addTodo })(AddTodo)
現在我們的 <AddTodo />
會連接到 store。當我們新增待辦事項時,它會 dispatch 一個 action 來變更 store。我們不會在應用程式中看到它,因為其他元件尚未連接。如果您已連接 Redux DevTools Extension,應該會看到 dispatch 的動作
您還應該看到 store 已相應地變更
<TodoList />
元件負責呈現待辦事項清單。因此,它需要從 store 讀取資料。我們可以透過呼叫具有 mapStateToProps
參數的 connect
來啟用它,此函式描述我們從 store 所需資料的哪一部分。
我們的 <Todo />
元件取待辦事項項目作為 prop。我們從 todos
的 byIds
欄位取得此資訊。不過,我們還需要 store 中 allIds
欄位的資訊,以指出應呈現哪些待辦事項以及以何種順序呈現。我們的 mapStateToProps
函式可以像這樣
// components/TodoList.js
// ...other imports
import { connect } from "react-redux";
const TodoList = // ... UI component implementation
const mapStateToProps = state => {
const { byIds, allIds } = state.todos || {};
const todos =
allIds && allIds.length
? allIds.map(id => (byIds ? { ...byIds[id], id } : null))
: null;
return { todos };
};
export default connect(mapStateToProps)(TodoList);
幸運的是,我們有選擇器可以做到這件事。我們可以直接匯入選擇器並在此處使用它。
// redux/selectors.js
export const getTodosState = (store) => store.todos
export const getTodoList = (store) =>
getTodosState(store) ? getTodosState(store).allIds : []
export const getTodoById = (store, id) =>
getTodosState(store) ? { ...getTodosState(store).byIds[id], id } : {}
export const getTodos = (store) =>
getTodoList(store).map((id) => getTodoById(store, id))
// components/TodoList.js
// ...other imports
import { connect } from "react-redux";
import { getTodos } from "../redux/selectors";
const TodoList = // ... UI component implementation
export default connect(state => ({ todos: getTodos(state) }))(TodoList);
我們建議將任何複雜的查詢或資料運算封裝在選擇器函式中。此外,您可以進一步使用 Reselect 來最佳化效能,撰寫可以略過不必要工作的「快取」選擇器。(請參閱 Redux 文件中的運算衍生資料頁面 和部落格文章 運用 Reselect 選擇器進行封裝和效能最佳化的慣用語彙 Redux,以取得更多關於為什麼以及如何使用選擇器函式的資訊。)
現在我們的 <TodoList />
已連接到 store。它應接收待辦事項清單,對它們進行對應,並將每個待辦事項傳遞給 <Todo />
元件。<Todo />
會反過來將它們呈現到畫面。現在試著新增待辦事項。它應該會顯示在我們的待辦事項清單中!
我們將連接更多元件。在這樣做之前,讓我們暫停並進一步了解一下 connect
。
呼叫 connect
的一般方法
根據您處理的元件類型,有不同的呼叫 connect
方式,以下是歸納出最常見的幾種方式
不訂閱 Store | 訂閱 Store | |
---|---|---|
不注入 Action Creator | connect()(Component) | connect(mapStateToProps)(Component) |
注入 Action Creator | connect(null, mapDispatchToProps)(Component) | connect(mapStateToProps, mapDispatchToProps)(Component) |
不訂閱 Store 也未注入 Action Creator
如果您沒有提供任何參數就呼叫 connect
,元件將
- 不會 在 Store 變更時重新渲染
- 接收
props.dispatch
,您可以使用它手動發送動作
// ... Component
export default connect()(Component) // Component will receive `dispatch` (just like our <TodoList />!)
訂閱 Store 但未注入 Action Creator
如果您僅以 mapStateToProps
呼叫 connect
,元件將
- 訂閱
mapStateToProps
從 Store 中提取的值,並僅在這些值變更時重新渲染 - 接收
props.dispatch
,您可以使用它手動發送動作
// ... Component
const mapStateToProps = (state) => state.partOfState
export default connect(mapStateToProps)(Component)
不訂閱 Store 並注入 Action Creator
如果您僅以 mapDispatchToProps
呼叫 connect
,元件將
- 不會 在 Store 變更時重新渲染
- 接收您以
mapDispatchToProps
注入的每個 Action Creator 作為道具,並在被呼叫時自動發送動作
import { addTodo } from './actionCreators'
// ... Component
export default connect(null, { addTodo })(Component)
訂閱 Store 並注入 Action Creator
如果您同時以 mapStateToProps
和 mapDispatchToProps
呼叫 connect
,元件將
- 訂閱
mapStateToProps
從 Store 中提取的值,並僅在這些值變更時重新渲染 - 接收您以
mapDispatchToProps
注入的所有 Action Creator 作為道具,並在被呼叫時自動發送動作。
import * as actionCreators from './actionCreators'
// ... Component
const mapStateToProps = (state) => state.partOfState
export default connect(mapStateToProps, actionCreators)(Component)
這四個案例涵蓋了 connect
最基本的用法。要進一步了解 connect
,請繼續閱讀我們的 API 部分,其中提供了更詳細的說明。
現在讓我們連接我們 <TodoApp />
的其餘部分。
我們應該如何實作切換待辦事項的互動?細心的閱讀者可能已經有答案了。如果您已設定好環境,而且已遵循指引進行到這裡,現在是一個好的時機,先放一邊,自行實作此功能。我們會將 <Todo />
與 toggleTodo
進行連結,並且以類似的方式發送資料,這一點並不會令人意外
// components/Todo.js
// ... other imports
import { connect } from "react-redux";
import { toggleTodo } from "../redux/actions";
const Todo = // ... component implementation
export default connect(
null,
{ toggleTodo }
)(Todo);
現在,我們的待辦事項狀態可以切換為已完成,我們快完成啦!
最後,讓我們實作我們的 VisibilityFilters
功能。
<VisibilityFilters />
元件需要能夠從資料庫中讀取目前使用中的篩選器,並對資料庫發送動作。因此,我們需要同時傳送一個 mapStateToProps
與 mapDispatchToProps
。這裡的 mapStateToProps
可以是 visibilityFilter
狀態的一個簡單存取器。而 mapDispatchToProps
將包含一個 setFilter
動作建立器。
// components/VisibilityFilters.js
// ... other imports
import { connect } from "react-redux";
import { setFilter } from "../redux/actions";
const VisibilityFilters = // ... component implementation
const mapStateToProps = state => {
return { activeFilter: state.visibilityFilter };
};
export default connect(
mapStateToProps,
{ setFilter }
)(VisibilityFilters);
與此同時,我們也需要更新我們的 <TodoList />
元件,以根據目前的篩選器來篩選待辦事項。之前我們傳遞給 <TodoList />
connect
函數呼叫的 mapStateToProps
,只是一個選取所有待辦事項清單的選擇器。讓我們寫另一個選擇器,以協助根據這些待辦事項的狀態進行篩選。
// redux/selectors.js
// ... other selectors
export const getTodosByVisibilityFilter = (store, visibilityFilter) => {
const allTodos = getTodos(store)
switch (visibilityFilter) {
case VISIBILITY_FILTERS.COMPLETED:
return allTodos.filter((todo) => todo.completed)
case VISIBILITY_FILTERS.INCOMPLETE:
return allTodos.filter((todo) => !todo.completed)
case VISIBILITY_FILTERS.ALL:
default:
return allTodos
}
}
並使用這個選擇器連線到資料庫
// components/TodoList.js
// ...
const mapStateToProps = (state) => {
const { visibilityFilter } = state
const todos = getTodosByVisibilityFilter(state, visibilityFilter)
return { todos }
}
export default connect(mapStateToProps)(TodoList)
現在我們完成了一個非常簡單的 React Redux 待辦事項應用程式範例。我們所有的元件都已連線!是不是很棒? 🎉🎊
連結
取得更多協助
- Reactiflux Redux 管道
- StackOverflow
- GitHub 問題