跳至主要內容

教學:使用 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 我們的應用程式的進入元件。它會呈現標頭、AddTodoTodoListVisibilityFilters 元件。
  • AddTodo 是讓使用者輸入待辦事項,並在按下「新增待辦事項」按鈕後將其加入清單的元件
    • 它使用受控輸入來設定 onChange 時的狀態。
    • 當使用者按下「新增待辦事項」按鈕時,它會傳送動作(我們會使用 React Redux 來提供),將待辦事項加到儲存中。
  • TodoList 是呈現待辦事項清單的元件
    • 當選取其中一個 VisibilityFilters 時,它會呈現已過濾的待辦事項清單。
  • Todo 是呈現單一待辦事項的元件
    • 它會呈現待辦事項內容,並透過刪除線來顯示待辦事項已完成。
    • 它會傳送動作來切換待辦事項的完成狀態,在 onClick 時。
  • VisibilityFilters 呈現簡單的一組篩選條件:全部已完成未完成。 點選其中一個會篩選待辦事項
    • 它接受父層的 activeFilter 屬性,用來表示使用者目前選取的篩選條件。使用底線來呈現目前的篩選條件。
    • 它會傳送 setFilter 動作來更新選取的篩選條件。
  • constants 中包含我們應用程式的常數資料。
  • 最後,index 會將我們的應用程式呈現到 DOM 中。

Redux 儲存

這個應用程式的 Redux 部分使用 Redux 文件中建議的模式 來設定

  • 儲存
    • todos:一個正規化的待辦事項的簡化器。它包含一個 byIds 地圖,用來儲存所有待辦事項,以及一個 allIds 清單,用來儲存所有識別碼的清單。
    • visibilityFilters:一個簡單的字串,可能是 allcompletedincomplete
  • 動作產生器
    • addTodo 建立一個動作來新增待辦事項。它會接收一個字串變數 content,並回傳一個 ADD_TODO 動作,其 payload 中包含一個自動遞增的 idcontent
    • 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 來儲存動作類型常數,俾供重複使用
  • 選擇器
    • getTodoListtodos 儲存單位傳回 allIds 清單
    • getTodoById 在儲存單位中找到指定為 id 的 to-do
    • getTodos 稍微複雜一點。函數會從 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。我們從 todosbyIds 欄位取得此資訊。不過,我們還需要 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 Creatorconnect()(Component)connect(mapStateToProps)(Component)
注入 Action Creatorconnect(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

如果您同時以 mapStateToPropsmapDispatchToProps 呼叫 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 /> 元件需要能夠從資料庫中讀取目前使用中的篩選器,並對資料庫發送動作。因此,我們需要同時傳送一個 mapStateToPropsmapDispatchToProps。這裡的 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 待辦事項應用程式範例。我們所有的元件都已連線!是不是很棒? 🎉🎊

取得更多協助