Redux 精華,第 4 部分:使用 Redux 資料
- 在多個 React 元件中使用 Redux 資料
- 組織執行動作的邏輯
- 在 reducer 中撰寫更複雜的更新邏輯
簡介
在 第 3 部分:Redux 基本資料流程 中,我們看過如何從空的 Redux+React 專案設定開始,新增一個新的狀態區塊,以及建立可以從 Redux 儲存庫讀取資料和發送動作來更新資料的 React 元件。我們也看過資料如何在應用程式中流動,元件發送動作、Reducer 處理動作並傳回新的狀態,以及元件讀取新的狀態並重新渲染 UI。
現在您已知道撰寫 Redux 邏輯的核心步驟,我們將使用相同的步驟為我們的社群媒體動態新增一些新功能,讓它更實用:檢視單一貼文、編輯現有貼文、顯示貼文作者詳細資料、貼文時間戳記和反應按鈕。
提醒您,程式碼範例重點在於每個區段中的關鍵概念和變更。請參閱 CodeSandbox 專案和 專案儲存庫中的 tutorial-steps
分支,以了解應用程式中的完整變更。
顯示單一貼文
由於我們有能力將新貼文新增到 Redux 儲存庫,因此我們可以新增一些以不同方式使用貼文資料的新功能。
目前,我們的貼文條目顯示在主動態頁面上,但如果文字太長,我們只會顯示內容的摘要。能夠在單獨的頁面上檢視單一貼文條目會很有幫助。
建立單一貼文頁面
首先,我們需要將新的 SinglePostPage
元件新增到我們的 posts
功能資料夾。我們將使用 React Router 在頁面 URL 看起來像 /posts/123
時顯示此元件,其中 123
部分應該是我們要顯示的貼文 ID。
import React from 'react'
import { useSelector } from 'react-redux'
export const SinglePostPage = ({ match }) => {
const { postId } = match.params
const post = useSelector(state =>
state.posts.find(post => post.id === postId)
)
if (!post) {
return (
<section>
<h2>Post not found!</h2>
</section>
)
}
return (
<section>
<article className="post">
<h2>{post.title}</h2>
<p className="post-content">{post.content}</p>
</article>
</section>
)
}
React Router 會傳入一個 match
物件作為道具,其中包含我們要尋找的 URL 資訊。當我們設定路由來呈現此元件時,我們會告訴它將 URL 的第二部分解析為一個名為 postId
的變數,並且我們可以從 match.params
讀取該值。
一旦我們有 postId
值,我們可以在選擇器函數中使用它,從 Redux 儲存中找到正確的貼文物件。我們知道 state.posts
應該是所有貼文物件的陣列,因此我們可以使用 Array.find()
函數來迴圈陣列,並傳回我們正在尋找的 ID 的貼文條目。
重要的是要注意,元件會在從 useSelector
傳回的值變更為新參照時重新渲染。元件應始終嘗試從儲存中選擇最少量的所需資料,這將有助於確保它只在實際需要時才渲染。
我們可能在儲存中沒有匹配的貼文條目 - 也許使用者嘗試直接在 URL 中輸入,或者我們沒有載入正確的資料。如果發生這種情況,find()
函數將傳回 undefined
,而不是實際的貼文物件。我們的元件需要檢查並透過在頁面中顯示「找不到貼文!」訊息來處理它。
假設我們在儲存中有正確的貼文物件,useSelector
將傳回該物件,我們可以使用它來渲染頁面中貼文的標題和內容。
您可能會注意到,這看起來與我們在 <PostsList>
元件主體中的邏輯非常相似,我們在其中迴圈整個 posts
陣列,以在主饋送中顯示貼文摘錄。我們可以嘗試提取可在兩個地方使用的 Post
元件,但我們顯示貼文摘錄和完整貼文的方式已經存在一些差異。即使存在一些重複,通常最好先將事情分開撰寫一段時間,然後我們稍後可以決定不同的程式碼區段是否足夠相似,以至於我們可以真正提取可重複使用的元件。
新增單一貼文路由
現在我們有一個 <SinglePostPage>
元件,我們可以定義一個路由來顯示它,並在首頁饋送中新增連結至每則貼文。
我們將在 App.js
中匯入 SinglePostPage
,並新增路由
import { PostsList } from './features/posts/PostsList'
import { AddPostForm } from './features/posts/AddPostForm'
import { SinglePostPage } from './features/posts/SinglePostPage'
function App() {
return (
<Router>
<Navbar />
<div className="App">
<Switch>
<Route
exact
path="/"
render={() => (
<React.Fragment>
<AddPostForm />
<PostsList />
</React.Fragment>
)}
/>
<Route exact path="/posts/:postId" component={SinglePostPage} />
<Redirect to="/" />
</Switch>
</div>
</Router>
)
}
然後,在 <PostsList>
中,我們將更新清單渲染邏輯,以包含路由至特定貼文的 <Link>
import React from 'react'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
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>
<Link to={`/posts/${post.id}`} className="button muted-button">
View Post
</Link>
</article>
))
return (
<section className="posts-list">
<h2>Posts</h2>
{renderedPosts}
</section>
)
}
由於我們現在可以點擊到不同的頁面,在 <Navbar>
元件中提供一個連結回到主要文章頁面也會很有幫助
import React from 'react'
import { Link } from 'react-router-dom'
export const Navbar = () => {
return (
<nav>
<section>
<h1>Redux Essentials Example</h1>
<div className="navContent">
<div className="navLinks">
<Link to="/">Posts</Link>
</div>
</div>
</section>
</nav>
)
}
編輯文章
身為使用者,在完成撰寫文章、儲存後,才發現某處有錯誤,這真的令人沮喪。在建立文章後能夠編輯文章會很有用。
讓我們新增一個新的 <EditPostForm>
元件,它能夠取得現有的文章 ID,從儲存庫中讀取該文章,讓使用者編輯標題和文章內容,然後儲存變更以更新儲存庫中的文章。
更新文章項目
首先,我們需要更新 postsSlice
以建立新的簡化器函式和動作,讓儲存庫知道如何實際更新文章。
在 createSlice()
呼叫內,我們應該在 reducers
物件中新增一個新的函式。請記住,這個簡化器的名稱應該清楚描述發生了什麼事,因為我們會在 Redux DevTools 中看到簡化器名稱顯示為動作類型字串的一部分,只要這個動作被傳送。我們的第一个簡化器稱為 postAdded
,所以讓我們將這個稱為 postUpdated
。
為了更新文章物件,我們需要知道
- 正在更新的文章 ID,以便我們可以在狀態中找到正確的文章物件
- 使用者輸入的新
title
和content
欄位
Redux 動作物件需要有 type
欄位,它通常是一個描述性的字串,也可能包含其他欄位,提供有關發生了什麼事的更多資訊。根據慣例,我們通常將額外資訊放入稱為 action.payload
的欄位中,但由我們決定 payload
欄位包含什麼內容 - 它可以是字串、數字、物件、陣列或其他東西。在這種情況下,由於我們需要三項資訊,因此我們計畫讓 payload
欄位成為一個物件,其中包含三個欄位。這表示動作物件看起來像 {type: 'posts/postUpdated', payload: {id, title, content}}
。
預設情況下,由 createSlice
產生的動作建立器預期您傳入一個引數,而該值將作為 action.payload
放入動作物件中。因此,我們可以傳入包含這些欄位的物件作為 postUpdated
動作建立器的引數。
我們也知道 reducer 負責在執行動作時決定如何實際更新狀態。有鑑於此,我們應該讓 reducer 根據 ID 找出正確的貼文物件,並特別更新該貼文中的 title
和 content
欄位。
最後,我們需要匯出 createSlice
為我們產生的動作建立函式,以便使用者儲存貼文時,UI 可以執行新的 postUpdated
動作。
考量到所有這些需求,以下是我們完成後 postsSlice
定義應有的樣貌
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded(state, action) {
state.push(action.payload)
},
postUpdated(state, action) {
const { id, title, content } = action.payload
const existingPost = state.find(post => post.id === id)
if (existingPost) {
existingPost.title = title
existingPost.content = content
}
}
}
})
export const { postAdded, postUpdated } = postsSlice.actions
export default postsSlice.reducer
建立編輯貼文表單
我們新的 <EditPostForm>
元件看起來會類似於 <AddPostForm>
,但邏輯需要有點不同。我們需要從儲存區中擷取正確的 post
物件,然後使用它來初始化元件中的狀態欄位,以便使用者可以進行變更。使用者完成後,我們會將變更後的標題和內容值儲存回儲存區。我們也會使用 React Router 的歷程記錄 API 切換到單一貼文頁面並顯示該貼文。
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useHistory } from 'react-router-dom'
import { postUpdated } from './postsSlice'
export const EditPostForm = ({ match }) => {
const { postId } = match.params
const post = useSelector(state =>
state.posts.find(post => post.id === postId)
)
const [title, setTitle] = useState(post.title)
const [content, setContent] = useState(post.content)
const dispatch = useDispatch()
const history = useHistory()
const onTitleChanged = e => setTitle(e.target.value)
const onContentChanged = e => setContent(e.target.value)
const onSavePostClicked = () => {
if (title && content) {
dispatch(postUpdated({ id: postId, title, content }))
history.push(`/posts/${postId}`)
}
}
return (
<section>
<h2>Edit Post</h2>
<form>
<label htmlFor="postTitle">Post Title:</label>
<input
type="text"
id="postTitle"
name="postTitle"
placeholder="What's on your mind?"
value={title}
onChange={onTitleChanged}
/>
<label htmlFor="postContent">Content:</label>
<textarea
id="postContent"
name="postContent"
value={content}
onChange={onContentChanged}
/>
</form>
<button type="button" onClick={onSavePostClicked}>
Save Post
</button>
</section>
)
}
如同 SinglePostPage
,我們需要將它匯入 App.js
,並新增一個路由,使用 postId
作為路由參數來呈現此元件。
import { PostsList } from './features/posts/PostsList'
import { AddPostForm } from './features/posts/AddPostForm'
import { SinglePostPage } from './features/posts/SinglePostPage'
import { EditPostForm } from './features/posts/EditPostForm'
function App() {
return (
<Router>
<Navbar />
<div className="App">
<Switch>
<Route
exact
path="/"
render={() => (
<React.Fragment>
<AddPostForm />
<PostsList />
</React.Fragment>
)}
/>
<Route exact path="/posts/:postId" component={SinglePostPage} />
<Route exact path="/editPost/:postId" component={EditPostForm} />
<Redirect to="/" />
</Switch>
</div>
</Router>
)
}
我們也應該在 SinglePostPage
中新增一個新的連結,連結到 EditPostForm
,如下所示
import { Link } from 'react-router-dom'
export const SinglePostPage = ({ match }) => {
// omit other contents
<p className="post-content">{post.content}</p>
<Link to={`/editPost/${post.id}`} className="button">
Edit Post
</Link>
準備動作酬載
我們剛才看到,來自 createSlice
的動作建立函式通常會預期一個參數,這個參數會變成 action.payload
。這簡化了最常見的使用模式,但有時我們需要做更多工作來準備動作物件的內容。在我們的 postAdded
動作中,我們需要為新貼文產生一個唯一的 ID,我們也需要確定酬載是一個看起來像 {id, title, content}
的物件。
現在,我們在 React 元件中產生 ID 並建立酬載物件,然後將酬載物件傳遞到 postAdded
。但是,如果我們需要從不同的元件執行相同的動作,或者準備酬載的邏輯很複雜,該怎麼辦?我們必須在每次想要執行動作時重複該邏輯,而且我們強迫元件確切知道此動作的酬載應是什麼樣子。
如果動作需要包含唯一的 ID 或其他隨機值,請務必先產生該值並將其放入動作物件中。reducer 永遠不應該計算隨機值,因為這會讓結果無法預測。
如果我們手動撰寫 postAdded
動作建立器,我們可以將設定邏輯放入其中
// hand-written action creator
function postAdded(title, content) {
const id = nanoid()
return {
type: 'posts/postAdded',
payload: { id, title, content }
}
}
但是,Redux Toolkit 的 createSlice
會為我們產生這些動作建立器。這會縮短程式碼,因為我們不必自己撰寫,但我們仍然需要一種方法來自訂 action.payload
的內容。
幸運的是,createSlice
讓我們在撰寫 reducer 時定義一個「準備回呼」函式。這個「準備回呼」函式可以接受多個參數,產生隨機值(例如唯一 ID),並執行任何其他同步邏輯以決定哪些值會放入動作物件中。然後它應該傳回一個物件,其中包含 payload
欄位。(傳回物件也可以包含 meta
欄位,可用於為動作新增額外的描述性值,以及 error
欄位,它應該是一個布林值,表示這個動作是否代表某種錯誤。)
在 createSlice
中的 reducers
欄位中,我們可以將其中一個欄位定義為看起來像 {reducer, prepare}
的物件
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded: {
reducer(state, action) {
state.push(action.payload)
},
prepare(title, content) {
return {
payload: {
id: nanoid(),
title,
content
}
}
}
}
// other reducers here
}
})
現在,我們的元件不必擔心 payload 物件的樣貌,動作建立器會負責將它正確地組合在一起。因此,我們可以更新元件,讓它在派送 postAdded
時傳入 title
和 content
作為參數
const onSavePostClicked = () => {
if (title && content) {
dispatch(postAdded(title, content))
setTitle('')
setContent('')
}
}
使用者和文章
到目前為止,我們只有一個狀態區塊。邏輯定義在 postsSlice.js
中,資料儲存在 state.posts
中,我們所有的元件都與文章功能有關。實際應用程式可能會有許多不同的狀態區塊,以及幾個不同的 Redux 邏輯和 React 元件的「功能資料夾」。
如果沒有其他人參與,你就不會有「社群媒體」應用程式。讓我們新增追蹤應用程式中使用者清單的功能,並更新與文章相關的功能以使用該資料。
新增使用者區塊
由於「使用者」的概念不同於「文章」的概念,我們希望將使用者的程式碼和資料與文章的程式碼和資料分開。我們將新增一個新的 features/users
資料夾,並在其中放置 usersSlice
檔案。與文章區塊一樣,現在我們將新增一些初始項目,以便我們有資料可以使用。
import { createSlice } from '@reduxjs/toolkit'
const initialState = [
{ id: '0', name: 'Tianna Jenkins' },
{ id: '1', name: 'Kevin Grant' },
{ id: '2', name: 'Madison Price' }
]
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {}
})
export default usersSlice.reducer
現在,我們不需要實際更新資料,所以我們會將 reducers
欄位留空。(我們將在後面的章節中回頭處理這一點。)
和之前一樣,我們會將 usersReducer
匯入我們的儲存庫檔案,並將其新增到儲存庫設定中
import { configureStore } from '@reduxjs/toolkit'
import postsReducer from '../features/posts/postsSlice'
import usersReducer from '../features/users/usersSlice'
export default configureStore({
reducer: {
posts: postsReducer,
users: usersReducer
}
})
為文章新增作者
我們應用程式中的每篇文章都是由我們的使用者之一所撰寫,而每次新增一篇文章時,我們都應該追蹤是哪個使用者撰寫該篇文章。在真實的應用程式中,我們會有一個 state.currentUser
欄位來追蹤目前登入的使用者,並在他們新增文章時使用該資訊。
為了讓這個範例更簡單,我們會更新我們的 <AddPostForm>
元件,以便我們可以從下拉式清單中選擇使用者,並且我們會將該使用者的 ID 包含在文章中。一旦我們的文章物件包含使用者 ID,我們就可以使用它來查詢使用者的名稱,並在使用者介面的每個個別文章中顯示它。
首先,我們需要更新我們的 postAdded
動作建立器,以接受使用者 ID 作為引數,並將其包含在動作中。(我們還會更新 initialState
中現有的文章項目,讓它們有一個包含範例使用者 ID 之一的 post.user
欄位。)
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded: {
reducer(state, action) {
state.push(action.payload)
},
prepare(title, content, userId) {
return {
payload: {
id: nanoid(),
title,
content,
user: userId
}
}
}
}
// other reducers
}
})
現在,在我們的 <AddPostForm>
中,我們可以使用 useSelector
從儲存庫中讀取使用者清單,並將它們顯示為下拉式選單。然後,我們會取得所選使用者的 ID,並將其傳遞給 postAdded
動作建立器。同時,我們可以為我們的表單新增一些驗證邏輯,以便使用者只有在標題和內容輸入欄位中輸入一些實際文字時,才能按一下「儲存文章」按鈕
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { postAdded } from './postsSlice'
export const AddPostForm = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [userId, setUserId] = useState('')
const dispatch = useDispatch()
const users = useSelector(state => state.users)
const onTitleChanged = e => setTitle(e.target.value)
const onContentChanged = e => setContent(e.target.value)
const onAuthorChanged = e => setUserId(e.target.value)
const onSavePostClicked = () => {
if (title && content) {
dispatch(postAdded(title, content, userId))
setTitle('')
setContent('')
}
}
const canSave = Boolean(title) && Boolean(content) && Boolean(userId)
const usersOptions = users.map(user => (
<option key={user.id} value={user.id}>
{user.name}
</option>
))
return (
<section>
<h2>Add a New Post</h2>
<form>
<label htmlFor="postTitle">Post Title:</label>
<input
type="text"
id="postTitle"
name="postTitle"
placeholder="What's on your mind?"
value={title}
onChange={onTitleChanged}
/>
<label htmlFor="postAuthor">Author:</label>
<select id="postAuthor" value={userId} onChange={onAuthorChanged}>
<option value=""></option>
{usersOptions}
</select>
<label htmlFor="postContent">Content:</label>
<textarea
id="postContent"
name="postContent"
value={content}
onChange={onContentChanged}
/>
<button type="button" onClick={onSavePostClicked} disabled={!canSave}>
Save Post
</button>
</form>
</section>
)
}
現在,我們需要一個方法來顯示文章清單項目和 <SinglePostPage>
中文章作者的名稱。由於我們希望在多個地方顯示相同類型的資訊,因此我們可以建立一個 PostAuthor
元件,它會將使用者 ID 作為道具,查詢正確的使用者物件,並格式化使用者的名稱
import React from 'react'
import { useSelector } from 'react-redux'
export const PostAuthor = ({ userId }) => {
const author = useSelector(state =>
state.users.find(user => user.id === userId)
)
return <span>by {author ? author.name : 'Unknown author'}</span>
}
請注意,我們在每個元件中都遵循相同的模式。任何需要從 Redux 儲存區讀取資料的元件都可以使用 useSelector
勾子,並擷取它需要的特定資料片段。此外,許多元件可以同時存取 Redux 儲存區中的相同資料。
我們現在可以將 PostAuthor
元件匯入到 PostsList.js
和 SinglePostPage.js
中,並將其呈現為 <PostAuthor userId={post.user} />
,每次新增文章條目時,所選使用者的名稱都應顯示在呈現的文章中。
更多文章功能
在這個階段,我們可以建立和編輯文章。讓我們新增一些額外的邏輯,讓我們的文章饋送更有用。
儲存文章日期
社群媒體饋送通常會按文章建立時間排序,並以相對描述的方式顯示文章建立時間,例如「5 小時前」。為此,我們需要開始追蹤文章條目的 date
欄位。
與 post.user
欄位一樣,我們將更新 postAdded
準備回呼,以確保在執行動作時總是包含 post.date
。但是,它不是將傳入的另一個參數。我們希望始終使用執行動作時的確切時間戳記,因此我們將讓準備回呼自行處理。
Redux 動作和狀態應僅包含純粹的 JS 值,例如物件、陣列和基本型別。不要將類別實例、函式或其他不可序列化值放入 Redux!.
由於我們無法將 Date
類別實例放入 Redux 儲存區,因此我們將追蹤 post.date
值作為時間戳記字串
postAdded: {
reducer(state, action) {
state.push(action.payload)
},
prepare(title, content, userId) {
return {
payload: {
id: nanoid(),
date: new Date().toISOString(),
title,
content,
user: userId,
},
}
},
},
與文章作者一樣,我們需要在 <PostsList>
和 <SinglePostPage>
元件中顯示相對時間戳記描述。date-fns
等函式庫有一些有用的公用程式函式,可用於剖析和格式化日期,我們可以在這裡使用它們
import React from 'react'
import { parseISO, formatDistanceToNow } from 'date-fns'
export const TimeAgo = ({ timestamp }) => {
let timeAgo = ''
if (timestamp) {
const date = parseISO(timestamp)
const timePeriod = formatDistanceToNow(date)
timeAgo = `${timePeriod} ago`
}
return (
<span title={timestamp}>
<i>{timeAgo}</i>
</span>
)
}
排序文章清單
我們的 <PostsList>
目前顯示所有文章,其順序與 Redux 儲存在儲存區中的文章順序相同。我們的範例最舊的文章顯示在最前面,而每次我們新增一篇文章時,都會將其新增至文章陣列的結尾。這表示最新文章永遠顯示在頁面的最下方。
社群媒體動態通常會先顯示最新文章,而您向下捲動才能看到較舊的文章。即使資料在儲存區中是以最舊文章顯示在最前面的方式儲存,我們仍可以在 <PostsList>
元件中重新排序資料,讓最新文章顯示在最前面。理論上,由於我們知道 state.posts
陣列已經排序,我們可以直接反轉清單。但是,最好還是自己先排序,以確保正確無誤。
由於 array.sort()
會變更現有陣列,因此我們需要複製 state.posts
並對複製的版本進行排序。我們知道 post.date
欄位儲存為日期時間戳記字串,我們可以直接比較這些欄位,以正確順序對文章進行排序
// Sort posts in reverse chronological order by datetime string
const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date))
const renderedPosts = orderedPosts.map(post => {
return (
<article className="post-excerpt" key={post.id}>
<h3>{post.title}</h3>
<div>
<PostAuthor userId={post.user} />
<TimeAgo timestamp={post.date} />
</div>
<p className="post-content">{post.content.substring(0, 100)}</p>
<Link to={`/posts/${post.id}`} className="button muted-button">
View Post
</Link>
</article>
)
})
我們還需要將 date
欄位新增至 postsSlice.js
中的 initialState
。我們會在此再次使用 date-fns
,從目前日期/時間中減去幾分鐘,讓它們彼此不同。
import { createSlice, nanoid } from '@reduxjs/toolkit'
import { sub } from 'date-fns'
const initialState = [
{
// omitted fields
content: 'Hello!',
date: sub(new Date(), { minutes: 10 }).toISOString()
},
{
// omitted fields
content: 'More text',
date: sub(new Date(), { minutes: 5 }).toISOString()
}
]
文章反應按鈕
我們還有一個新功能要新增至這個區段。目前,我們的文章有點無聊。我們需要讓它們更有趣,而讓朋友們為我們的文章新增反應表情符號,不就是最好的方法嗎?
我們會在 <PostsList>
和 <SinglePostPage>
中每個文章的底部新增一行表情符號反應按鈕。每次使用者按一下其中一個反應按鈕時,我們都需要更新 Redux 儲存區中該篇文章的對應計數器欄位。由於反應計數器資料儲存在 Redux 儲存區中,因此在應用程式不同部分之間切換時,應該會在使用該資料的任何元件中持續顯示相同的值。
與文章作者和時間戳記一樣,我們希望在顯示文章的所有地方都使用這個功能,因此我們會建立一個 <ReactionButtons>
元件,並將 post
作為道具。我們會從在內部顯示按鈕開始,並顯示每個按鈕目前的反應數
import React from 'react'
const reactionEmoji = {
thumbsUp: '👍',
hooray: '🎉',
heart: '❤️',
rocket: '🚀',
eyes: '👀'
}
export const ReactionButtons = ({ post }) => {
const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
return (
<button key={name} type="button" className="muted-button reaction-button">
{emoji} {post.reactions[name]}
</button>
)
})
return <div>{reactionButtons}</div>
}
我們的資料中尚未有 post.reactions
欄位,因此我們需要更新 initialState
文章物件和 postAdded
準備回呼函式,以確保每個文章都有該資料,例如 reactions: {thumbsUp: 0, hooray: 0, heart: 0, rocket: 0, eyes: 0}
。
現在,我們可以定義一個新的 reducer,它會在使用者按讚時處理更新文章的按讚數。
與編輯文章一樣,我們需要知道文章的 ID,以及使用者按讚哪個按鈕。我們的 action.payload
將會是一個看起來像 {id, reaction}
的物件。然後,reducer 可以找到正確的文章物件,並更新正確的反應欄位。
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
reactionAdded(state, action) {
const { postId, reaction } = action.payload
const existingPost = state.find(post => post.id === postId)
if (existingPost) {
existingPost.reactions[reaction]++
}
}
// other reducers
}
})
export const { postAdded, postUpdated, reactionAdded } = postsSlice.actions
正如我們已經看到的,createSlice
讓我們在 reducer 中撰寫「變異」邏輯。如果我們沒有使用 createSlice
和 Immer 函式庫,則 existingPost.reactions[reaction]++
這行會變異現有的 post.reactions
物件,而且這可能會在我們的應用程式中其他地方造成錯誤,因為我們沒有遵循 reducer 的規則。但是,由於我們有使用 createSlice
,我們可以用更簡單的方式撰寫這個更複雜的更新邏輯,並讓 Immer 將這段程式碼轉換成安全的不可變更新。
請注意,我們的動作物件僅包含描述發生事件所需的最小資訊量。我們知道需要更新哪篇文章,以及按下了哪個反應名稱。我們可以計算新的反應計數器值並將其放入動作中,但最好始終將動作物件保持在最小值,並在 reducer 中執行狀態更新計算。這也表示reducer 可以包含計算新狀態所需的邏輯。
我們的最後一步是更新 <ReactionButtons>
元件,以便在使用者按鈕時發送 reactionAdded
動作
import React from 'react'
import { useDispatch } from 'react-redux'
import { reactionAdded } from './postsSlice'
const reactionEmoji = {
thumbsUp: '👍',
hooray: '🎉',
heart: '❤️',
rocket: '🚀',
eyes: '👀'
}
export const ReactionButtons = ({ post }) => {
const dispatch = useDispatch()
const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
return (
<button
key={name}
type="button"
className="muted-button reaction-button"
onClick={() =>
dispatch(reactionAdded({ postId: post.id, reaction: name }))
}
>
{emoji} {post.reactions[name]}
</button>
)
})
return <div>{reactionButtons}</div>
}
現在,每次我們按一下反應按鈕時,計數器都會遞增。如果我們瀏覽到應用程式的不同部分,我們應該會看到正確的計數器值顯示在我們查看此文章的任何時間,即使我們按一下 <PostsList>
中的反應按鈕,然後在 <SinglePostPage>
中查看文章本身。
你學到什麼
以下是我們應用程式在所有這些變更後的樣子
它實際上開始看起來更有用、更有趣了!
我們在本節中涵蓋了很多資訊和概念。讓我們回顧一下要記住的重要事項
- 任何 React 元件都可以根據需要使用 Redux 儲存庫中的資料
- 任何元件都可以讀取 Redux 儲存庫中的任何資料
- 多個元件可以讀取相同的資料,即使在同一時間
- 元件應擷取渲染自身所需的最小資料量
- 元件可以結合來自道具、狀態和 Redux 儲存庫的值,以確定它們需要渲染的 UI。它們可以從儲存庫中讀取多個資料片段,並根據需要重新調整資料以供顯示。
- 任何元件都可以發送動作以導致狀態更新
- Redux 動作建立器可以準備具有正確內容的動作物件
createSlice
和createAction
可以接受傳回動作有效負載的「準備回呼」- 唯一的 ID 和其他隨機值應放入動作中,而不是在還原器中計算
- 還原器應包含實際的狀態更新邏輯
- 還原器可以包含計算下一個狀態所需的任何邏輯
- 動作物件應僅包含足夠的資訊來描述發生了什麼事
下一步?
現在你應該已經能輕鬆使用 Redux 儲存庫和 React 元件中的資料。到目前為止,我們只使用初始狀態中的資料或使用者新增的資料。在 第 5 部分:非同步邏輯和資料擷取 中,我們將了解如何使用來自伺服器 API 的資料。