跳到主要內容

Redux 精華,第 3 部分:Redux 基本資料流程

您將會學到
  • 如何使用 createSlice 將 reducer 邏輯的「切片」新增至 Redux store
  • 使用 useSelector 鉤子在元件中讀取 Redux 資料
  • 使用 useDispatch 鉤子在元件中派送動作
先備條件
  • 熟悉 Redux 的關鍵術語和概念,例如「動作」、「reducer」、「store」和「派送」。(請參閱 第 1 部分:Redux 概觀和概念 以了解這些術語的說明。)

簡介

第 1 部分:Redux 概觀和概念 中,我們探討了 Redux 如何透過提供一個集中放置全域應用程式狀態的單一中心位置,來協助我們建構可維護的應用程式。我們也探討了核心 Redux 概念,例如派送動作物件、使用會傳回新狀態值的 reducer 函式,以及使用 thunk 撰寫非同步邏輯。在 第 2 部分:Redux Toolkit 應用程式結構 中,我們了解了 Redux Toolkit 中的 configureStorecreateSlice 等 API,以及 React-Redux 中的 ProvideruseSelector 如何共同運作,讓我們可以撰寫 Redux 邏輯,並從我們的 React 元件與該邏輯互動。

現在您對這些部分有些概念,是時候將這些知識付諸實行了。我們將建構一個小型社群媒體 feed 應用程式,其中將包含許多展示實際使用案例的功能。這將有助於您了解如何在自己的應用程式中使用 Redux。

注意事項

範例應用程式並非完整的可生產專案。目標是協助您學習 Redux API 和典型的使用模式,並透過一些有限的範例指引您正確的方向。此外,我們建構的一些早期部分,稍後將更新為展示執行事項的更好方法。請通讀整個教學課程,以查看所有使用中的概念。

專案設定

針對本教學課程,我們建立了一個預先設定好的入門專案,其中已設定好 React 和 Redux,包含一些預設樣式,並具有一個假的 REST API,這讓我們可以在應用程式中撰寫實際的 API 要求。你會將其用作撰寫實際應用程式程式碼的基礎。

若要開始,你可以開啟並分岔這個 CodeSandbox

你也可以從這個 Github 存放庫複製相同的專案。複製存放庫後,你可以使用 npm install 安裝專案的工具,並使用 npm start 啟動它。

如果你想查看我們將要建置的最終版本,你可以查看tutorial-steps 分支,或查看這個 CodeSandbox 中的最終版本

我們要感謝Tania Rascia,她的使用 Redux 與 React 教學課程激勵了本頁中的範例。它也使用她的Primitive UI CSS 入門 進行樣式設定。

建立新的 Redux + React 專案

完成本教學課程後,你可能會想嘗試處理你自己的專案。我們建議使用Redux Vite 範本作為建立新的 Redux + React 專案的最快方法。它已設定好 Redux Toolkit 和 React-Redux,使用你在第 1 部分中看到的「計數器」應用程式範例。這讓你無需新增 Redux 套件和設定儲存,就能直接開始撰寫實際的應用程式程式碼。

如果你想了解如何將 Redux 新增至專案的具體詳細資訊,請參閱此說明

詳細說明:將 Redux 新增至 React 專案

Redux Vite 範本已設定好 Redux Toolkit 和 React-Redux。如果你要從頭開始設定新的專案,而沒有使用該範本,請執行下列步驟

  • 新增 @reduxjs/toolkitreact-redux 套件
  • 使用 RTK 的 configureStore API 建立 Redux 儲存,並傳入至少一個 reducer 函式
  • 將 Redux 儲存匯入應用程式的進入點檔案 (例如 src/index.js)
  • 使用 React-Redux 的 <Provider> 元件包裝你的根 React 元件,如下所示
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

探索初始專案

讓我們快速了解一下初始專案包含哪些內容

  • /public:HTML 主機頁面範本和其他靜態檔案,例如圖示
  • /src
    • index.js:應用程式的進入點檔案。它會呈現 React-Redux <Provider> 元件和主要的 <App> 元件。
    • App.js:主要的應用程式元件。會呈現頂端的導覽列,並處理其他內容的用戶端路由。
    • index.css:完整應用程式的樣式
    • /api
      • client.js:一個小型 AJAX 要求用戶端,它允許我們進行 GET 和 POST 要求
      • server.js:提供我們的資料一個假的 REST API。我們的應用程式稍後會從這些假的端點擷取資料。
    • /app
      • Navbar.js:呈現頂端的標頭和導覽列內容
      • store.js:建立 Redux 儲存庫執行個體

如果你現在載入應用程式,你應該會看到標頭和歡迎訊息。我們也可以開啟 Redux DevTools 擴充功能,並看到我們的初始 Redux 狀態完全是空的。

有了這些,我們開始吧!

主要貼文串

我們社群媒體串流應用程式的主要功能將會是一串貼文。我們會在進行的過程中加入更多部分到這個功能,但首先,我們的首要目標是只在畫面上顯示貼文條目的清單。

建立貼文區塊

第一步是建立一個新的 Redux「區塊」,它將包含我們貼文的資料。一旦我們在 Redux 儲存庫中擁有那些資料,我們就可以建立 React 元件,在頁面上顯示那些資料。

src 內部,建立一個新的 features 資料夾,在 features 內部放一個 posts 資料夾,並新增一個名為 postsSlice.js 的新檔案。

我們將使用 Redux Toolkit createSlice 函式,建立一個知道如何處理我們貼文資料的簡化器函式。簡化器函式需要包含一些初始資料,以便 Redux 儲存庫在應用程式啟動時載入那些值。

現在,我們將建立一個陣列,裡面有一些假的貼文物件,以便我們可以開始新增使用者介面。

我們將匯入 createSlice,定義我們的初始貼文陣列,將它傳遞給 createSlice,並匯出 createSlice 為我們產生的貼文簡化器函式

features/posts/postsSlice.js
import { createSlice } from '@reduxjs/toolkit'

const initialState = [
{ id: '1', title: 'First Post!', content: 'Hello!' },
{ id: '2', title: 'Second Post', content: 'More text' }
]

const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {}
})

export default postsSlice.reducer

每次建立新的切片時,我們需要將其簡化器函數新增到 Redux 儲存庫。我們已經建立了一個 Redux 儲存庫,但目前裡面沒有任何資料。開啟 app/store.js,匯入 postsReducer 函數,並更新對 configureStore 的呼叫,讓 postsReducer 傳遞為名為 posts 的簡化器欄位

app/store.js
import { configureStore } from '@reduxjs/toolkit'

import postsReducer from '../features/posts/postsSlice'

export default configureStore({
reducer: {
posts: postsReducer
}
})

這會告訴 Redux 我們希望頂層狀態物件內部有一個名為 posts 的欄位,而 state.posts 的所有資料會在執行動作時由 postsReducer 函數更新。

我們可以透過開啟 Redux DevTools Extension 並查看目前的狀態內容來確認這是否有效

Initial posts state

顯示文章清單

現在我們在儲存庫中有一些文章資料,我們可以建立一個 React 元件來顯示文章清單。所有與我們的動態文章功能相關的程式碼都應該放在 posts 資料夾中,因此請繼續在其中建立一個名為 PostsList.js 的新檔案。

如果我們要呈現文章清單,我們需要從某處取得資料。React 元件可以使用 React-Redux 函式庫中的 useSelector 鉤子從 Redux 儲存庫中讀取資料。您撰寫的「選擇器函數」會以整個 Redux state 物件為參數呼叫,並應該傳回此元件從儲存庫中需要的特定資料。

我們的初始 PostsList 元件會從 Redux 儲存庫中讀取 state.posts 值,然後迴圈處理文章陣列並在螢幕上顯示每個文章

features/posts/PostsList.js
import React from 'react'
import { useSelector } from 'react-redux'

export const PostsList = () => {
const posts = useSelector(state => state.posts)

const renderedPosts = posts.map(post => (
<article className="post-excerpt" key={post.id}>
<h3>{post.title}</h3>
<p className="post-content">{post.content.substring(0, 100)}</p>
</article>
))

return (
<section className="posts-list">
<h2>Posts</h2>
{renderedPosts}
</section>
)
}

然後我們需要更新 App.js 中的路由,以便我們顯示 PostsList 元件,而不是「歡迎」訊息。將 PostsList 元件匯入 App.js,並將歡迎文字替換為 <PostsList />。我們也會將它包覆在 React 片段 中,因為我們很快會在主頁中新增其他內容

App.js
import React from 'react'
import {
BrowserRouter as Router,
Switch,
Route,
Redirect
} from 'react-router-dom'

import { Navbar } from './app/Navbar'

import { PostsList } from './features/posts/PostsList'

function App() {
return (
<Router>
<Navbar />
<div className="App">
<Switch>
<Route
exact
path="/"
render={() => (
<React.Fragment>
<PostsList />
</React.Fragment>
)}
/>
<Redirect to="/" />
</Switch>
</div>
</Router>
)
}

export default App

新增後,我們應用程式的首頁現在應該如下所示

Initial posts list

進度!我們已經在 Redux 儲存庫中新增了一些資料,並在 React 元件中顯示在螢幕上。

新增新文章

瀏覽別人寫的文章固然不錯,但我們希望能夠撰寫自己的文章。讓我們建立一個「新增文章」表單,讓我們撰寫文章並儲存。

我們會先建立一個空的表單並將其新增到頁面中。接著,我們會將表單連接到 Redux 儲存區,以便在我們按一下「儲存文章」按鈕時新增文章。

新增文章表單

在我們的 posts 資料夾中建立 AddPostForm.js。我們會為文章標題新增一個文字輸入欄位,並為文章內文新增一個文字區域。

features/posts/AddPostForm.js
import React, { useState } from 'react'

export const AddPostForm = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')

const onTitleChanged = e => setTitle(e.target.value)
const onContentChanged = e => setContent(e.target.value)

return (
<section>
<h2>Add a New Post</h2>
<form>
<label htmlFor="postTitle">Post Title:</label>
<input
type="text"
id="postTitle"
name="postTitle"
value={title}
onChange={onTitleChanged}
/>
<label htmlFor="postContent">Content:</label>
<textarea
id="postContent"
name="postContent"
value={content}
onChange={onContentChanged}
/>
<button type="button">Save Post</button>
</form>
</section>
)
}

將該元件匯入 App.js,並將其新增在 <PostsList /> 元件正上方

App.js
<Route
exact
path="/"
render={() => (
<React.Fragment>
<AddPostForm />
<PostsList />
</React.Fragment>
)}
/>

你應該會看到表單顯示在頁面標題正下方。

儲存文章條目

現在,讓我們更新我們的文章區段,將新的文章條目新增到 Redux 儲存區。

我們的文章區段負責處理所有文章資料的更新。在 createSlice 呼叫內,有一個稱為 reducers 的物件。目前它是空的。我們需要在其中新增一個 reducer 函式,以處理新增文章的情況。

reducers 內,新增一個名為 postAdded 的函式,它會接收兩個引數:目前的 state 值,以及已發出的 action 物件。由於文章區段知道它負責的資料,因此 state 引數會是文章陣列本身,而不是整個 Redux 狀態物件。

action 物件會將我們的新文章條目作為 action.payload 欄位,我們會將該新文章物件放入 state 陣列中。

當我們撰寫 postAdded reducer 函式時,createSlice 會自動產生一個具有相同名稱的「動作建立器」函式。我們可以匯出該動作建立器,並在我們的 UI 元件中使用它,以便在使用者按一下「儲存文章」時發出動作。

features/posts/postsSlice.js
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded(state, action) {
state.push(action.payload)
}
}
})

export const { postAdded } = postsSlice.actions

export default postsSlice.reducer
危險

請記住:reducer 函式必須始終透過建立副本,以不可變的方式建立新的狀態值!createSlice() 內呼叫 Array.push() 等變異函式,或修改物件欄位(例如 state.someField = someValue),是安全的,因為它會使用 Immer 函式庫在內部將這些變異轉換為安全的不可變更新,但請勿嘗試在 createSlice 外變異任何資料!

發出「文章已新增」動作

我們的 AddPostForm 有文字輸入框和一個「儲存貼文」按鈕,但按鈕目前還沒有任何功能。我們需要新增一個點擊處理常式,它會派送 postAdded 動作建立器,並傳入一個包含使用者所寫標題和內容的新貼文物件。

我們的貼文物件也需要有 id 欄位。目前,我們的初始測試貼文使用一些假數字作為 ID。我們可以撰寫一些程式碼來找出下一個遞增 ID 號碼應該是多少,但如果我們改為產生一個隨機的唯一 ID,會更好。Redux Toolkit 有 nanoid 函式,我們可以使用它來達成此目的。

資訊

我們會在 第 4 部分:使用 Redux 資料 中進一步說明如何產生 ID 和派送動作。

為了從元件派送動作,我們需要存取儲存體的 dispatch 函式。我們透過呼叫 React-Redux 的 useDispatch 鉤子來取得它。我們也需要將 postAdded 動作建立器匯入這個檔案。

一旦元件中可以使用 dispatch 函式,我們就可以在點擊處理常式中呼叫 dispatch(postAdded())。我們可以從 React 元件的 useState 鉤子取得標題和內容值,產生一個新的 ID,並將它們組合成一個新的貼文物件,傳遞給 postAdded()

features/posts/AddPostForm
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { nanoid } from '@reduxjs/toolkit'

import { postAdded } from './postsSlice'

export const AddPostForm = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')

const dispatch = useDispatch()

const onTitleChanged = e => setTitle(e.target.value)
const onContentChanged = e => setContent(e.target.value)

const onSavePostClicked = () => {
if (title && content) {
dispatch(
postAdded({
id: nanoid(),
title,
content
})
)

setTitle('')
setContent('')
}
}

return (
<section>
<h2>Add a New Post</h2>
<form>
{/* omit form inputs */}
<button type="button" onClick={onSavePostClicked}>
Save Post
</button>
</form>
</section>
)
}

現在,請試著輸入標題和一些文字,然後按一下「儲存貼文」。你應該會在貼文清單中看到該貼文的新項目。

恭喜!你剛剛建立了你的第一個可用的 React + Redux 應用程式!

這顯示了完整的 Redux 資料流程週期

  • 我們的貼文清單使用 useSelector 從儲存體讀取初始貼文組,並呈現初始使用者介面
  • 我們派送包含新貼文條目資料的 postAdded 動作
  • 貼文簡化器看到 postAdded 動作,並使用新條目更新貼文陣列
  • Redux 儲存體告訴使用者介面某些資料已變更
  • 貼文清單讀取更新後的貼文陣列,並重新呈現自己以顯示新貼文

我們在此之後新增的所有新功能都將遵循你在此看到的相同基本模式:新增狀態區段、撰寫簡化器函式、派送動作,以及根據 Redux 儲存體中的資料呈現使用者介面。

我們可以檢查 Redux DevTools 擴充功能,查看我們派送的動作,並查看 Redux 狀態如何根據該動作更新。如果我們在動作清單中按一下 "posts/postAdded" 條目,「動作」標籤應該會如下所示

postAdded action contents

「差異」標籤也應該會顯示 state.posts 新增了一項新項目,位於索引 2。

請注意,我們的 AddPostForm 元件內部有一些 React useState 鉤子,用於追蹤使用者輸入的標題和內容值。請記住,Redux 儲存體應該只包含應用程式視為「全域性」的資料!在本例中,只有 AddPostForm 需要知道輸入欄位的最新值,因此我們希望將該資料保留在 React 元件狀態中,而不是嘗試將暫時資料保留在 Redux 儲存體中。當使用者完成表單時,我們會派送一個 Redux 動作,根據使用者的輸入,使用最終值更新儲存體。

你學到了什麼

讓我們回顧一下你在本節中學到的內容

摘要
  • Redux 狀態由「reducer 函數」更新:
    • Reducer 始終以不可變的方式計算新狀態,方法是複製現有的狀態值,並使用新資料修改複製的內容
    • Redux Toolkit 的 createSlice 函數會為您產生「slice reducer」函數,並讓您撰寫「可變」程式碼,這些程式碼會轉換為安全的不可變更新
    • 這些 slice reducer 函數會新增到 configureStore 中的 reducer 欄位,並定義 Redux 儲存體中的資料和狀態欄位名稱
  • React 元件使用 useSelector 勾子從儲存體中讀取資料
    • Selector 函數接收整個 state 物件,並應傳回一個值
    • Redux 儲存體更新時,Selector 會重新執行,如果傳回的資料已變更,元件將重新呈現
  • React 元件使用 useDispatch 勾子傳送動作來更新儲存體
    • createSlice 會為我們新增到 slice 的每個 reducer 產生動作建立函數
    • 在元件中呼叫 dispatch(someActionCreator()) 來傳送動作
    • Reducer 會執行,檢查這個動作是否相關,並在適當的情況下傳回新狀態
    • 像表單輸入值這類暫時資料應保留為 React 元件狀態。當使用者完成表單時,傳送 Redux 動作來更新儲存體。

以下是目前應用程式的樣子

下一步?

現在您已了解基本的 Redux 資料流程,請繼續閱讀 第 4 部分:使用 Redux 資料,我們將為應用程式新增一些額外的功能,並了解如何使用儲存體中已有的資料。