Redux必须手册,第4部分:使用Redux数据

754 阅读8分钟

介绍

第3部分:基本Redux数据流中,我们了解了如何从一个空的Redux + React项目设置开始,添加新的state slice,以及创建可以从Redux store中读取数据并dispatch action以更新该数据的React组件。我们还研究了数据如何在应用程序中流动,组件dispatch action,reducer处理action并返回新state,以及组件读取新state并重新呈现UI。

既然你已经知道编写Redux逻辑的核心步骤,我们将使用相同的步骤向社交媒体流中添加一些新功能,这将使其更加有用:查看单个post,编辑现有post,显示post的作者的详细信息,发布时间戳和评价按钮。

信息

提醒一下,这些代码示例集中在每个部分的关键概念和更改上。有关应用程序中的完整更改,请参见CodeSandbox项目和项目仓库中的tutorial-steps分支

查看单个post

由于我们有能力向Redux store添加新post,因此我们可以添加更多功能,这些功能以不同方式使用posts数据。

目前,我们的post条目会显示在主流式页面上,但是如果文本太长,我们只会显示内容摘录,然后在post对应的自己的页面上查看详细内容。

创建一个独立的Post页面

首先,我们需要在我们的posts功能文件夹中添加一个新的SinglePostPage组件。当页面URL看起来类似于/posts/123时,我们将使用React Router来显示此组件,其中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对象作为prop,其中包含我们要查找的URL信息。当我们设置渲染该组件的路由时,我们将告诉它将URL的第二部分解析为名为postId的变量,并且可以从match.params中读取该值。

一旦有了该postId值,就可以在selector函数中使用它,以从Redux store中找到正确的post对象。我们知道state.posts应该是所有post对象的数组,因此我们可以使用Array.find()函数遍历该数组,并返回具有所需ID的post条目。

重要的是要注意,只要useSelector返回的值更改为新引用,该组件就会重新渲染。 组件应始终尝试从store中选择所需的最小数据量,这将有助于确保仅在实际需要时才渲染数据。

Store中可能没有匹配的帖子条目——可能是用户试图直接键入URL,或者我们没有加载正确的数据。如果发生这种情况,则find()函数将返回undefined而不是实际的post对象。我们的组件需要检查并通过在页面中显示“找不到帖子!”来处理它。

假设store中确实有正确的post对象,useSelector将返回该对象,我们可以使用它在页面中呈现帖子的标题和内容。

你可能会注意到,这看上去与<PostsList>组件主体中的逻辑非常相似,在该逻辑中,我们遍历整个posts数组以显示主要摘要的posts列表。我们可以尝试提取可以在两个地方使用的Post组件,但是在显示帖子摘要和整个帖子的方式上已经存在一些差异。即使有一些重复,通常最好还是分开编写一段时间,然后再决定以后代码的不同部分是否足够相似,以至于我们可以真正提取出可重用的组件。

给这个独立的Post页面添加路由

现在,我们有了一个<SinglePostPage>组件,我们可以定义一个路由来显示它,并在首页feed中添加指向每个帖子的链接。

我们将在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>组件中也有一个指向主posts页面的链接也将有所帮助:

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>
  )
}

编辑post

作为用户,完成一篇post,保存后才意识到在post的某个地方犯了错误是真的很让人恼火。创建post后具有编辑post的功能将非常有用。

让我们添加一个新的<EditPostForm>组件,该组件能够获取现有的post的ID,从store中读取该帖子,让用户编辑标题和帖子内容,然后保存更改以更新store中的帖子。

更新post条目

首先,我们需要更新我们的postsSlice,以创建一个新的reducer函数和一个action,以便store知道如何更新posts。

createSlice()调用内部,我们应将一个新函数添加到reducers对象中。请记住,该reducer的名称应该很好地描述了所发生的事情,因为无论何时dispatch此action,我们都将在Redux DevTools中看到该reducer名称显示为action type字符串的一部分。我们的第一个reducer称为postAdded,所以我们称这个新增的为postUpdated

为了更新post对象,我们需要知道:

  • 正在更新的post的ID,以便我们可以在state中找到合适的post对象
  • 用户输入的新titlecontent字段

Redux action对象必须具有type字段,该字段通常是描述性字符串,并且还可能包含其他字段,其中包含有关所发生事件的更多信息。按照惯例,我们通常将附加信息放在一个名为action.payload的字段中,但是要由我们决定payload字段包含的内容——它可以是字符串,数字,对象,数组或其他内容。在这种情况下,由于我们需要三部分信息,因此让我们计划将payload字段作为一个对象,其中包含三个字段。这意味着action对象将类似于{type: 'posts/postUpdated', payload: {id, title, content}}

默认情况下,由createSlice生成的action creator希望你传入一个参数,并且该值将作为action.payload放入action对象。因此,我们可以将包含这些字段的对象作为参数传递给postUpdated action creator。

我们还知道,reducer负责确定在dispatch action时应如何更新state。鉴于此,我们应该让reducer根据ID找到正确的post对象,并专门更新该post中的title和content字段。

最后,我们需要导出createSlice为我们生成的action creator函数,以便用户保存post时UI可以调度新的postUpdated action。

考虑到所有这些要求,完成后,这是我们的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

创建一个编辑post表单

我们新的<EditPostForm>组件看起来与<AddPostForm>相似,但是逻辑需要有所不同。我们需要从store中检索正确的post对象,然后使用该对象来初始化组件中的state字段,以便用户进行更改。用户完成操作后,我们会将更改后的标题和内容值保存回store。我们还将使用React Router的历史记录API切换到单个post页面并显示该post。

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并添加一条显示此组件的路由。我们还应该向我们的SinglePostPage添加一个新链接,该链接将路由到EditPostPage,例如:

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>

准备action的payload

我们只是看到createSlice的action creator通常有一个参数,即action.payload。这是简化的最常见的用法模式,但是有时我们需要做更多的工作来准备action对象的内容。对于postAdded动作,我们需要为新post生成唯一的ID,并且还需要确保payload是一个看起来像{id,title,content}的对象。

现在,我们正在生成ID并在React组件中创建payloa对象,并将payloa对象传递到postAdded中。但是,如果我们需要从不同的组件dispatch相同的action,或者准备payloa的逻辑很复杂怎么办?每次我们想要dispatch action时,我们都必须重复该逻辑,并且我们迫使组件确切知道该action的payloa应该是什么样子。

警告

如果action需要包含唯一ID或其他随机值,请始终首先生成该ID并将其放入action对象中。Reducer永远不要计算随机值,因为这样会使结果不可预测。

如果我们是手动编写postAdded的action creator,则可以将设置逻辑本身放入其中:

// hand-written action creator
function postAdded(title, content) {
  const id = nanoid()
  return {
    type: 'posts/postAdded',
    payload: { id, title, content }
  }
}

但是,Redux Toolkit的createSlice正在为我们生成这些action creator。这使代码更短,因为我们不必自己编写代码,但是我们仍然需要一种方法来定制action.payload的内容。

幸运的是,当我们编写一个reducer时,createSlice允许我们定义一个“prepare callback”函数。“prepare callback”函数可以接受多个参数,生成诸如唯一ID之类的随机值,并运行需要其他任何同步逻辑来确定将哪些值放入action对象中。然后,它应返回一个内部包含payload字段的对象。(返回对象可能还包含一个meta字段和一个error字段,该meta字段可用于向该action添加额外的描述性值,而error字段应为一个布尔值,指示此action是否表示某种错误。)

createSlicereducers字段内,我们可以将其中一个字段定义为一个看起来像{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对象是什么样子——action creator将负责以正确的方式将其组合在一起。因此,我们可以更新组件,以便在dispatch postAdded时将titlecontent作为参数传递:

const onSavePostClicked = () => {
  if (title && content) {
    dispatch(postAdded(title, content))
    setTitle('')
    setContent('')
  }
}

Users和posts

到目前为止,我们只有一个state。该逻辑在postsSlice.js中定义,数据存储在state.posts中,并且我们所有的组件都与posts功能相关。实际的应用程序可能会有许多不同的state slice,以及Redux逻辑和React组件的几个不同的“功能文件夹”。

如果没有其他人参与,那么你的“社交媒体”应用程序没有任何意义。让我们添加一种功能来跟踪我们应用程序中的users列表,并更新与posts相关的功能以利用该数据。

添加一个users slice

由于“users”的概念与“posts”的概念不同,因此我们希望将users的代码和数据与posts的代码和数据分开。我们将添加一个新的features/users文件夹,并在其中放置一个usersSlice文件。像posts slice一样,现在我们将添加一些初始条目,以便我们可以使用数据。

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导入到我们的store文件中并将其添加到store设置中:

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
  }
})

添加post的作者

我们应用中的每个post都是由我们的一个user撰写的,每次添加新post时,我们都应跟踪哪个user撰写了该post。 在真实的应用程序中,我们会有某种state.currentUser字段来跟踪当前登录的用户,并在他们添加post时使用该信息。

为了使本示例更简单,我们将更新<AddPostForm>组件,以便我们可以从下拉列表中选择一个user,并将该user的ID包含在post中。 一旦我们的post对象中包含user的ID,我们就可以使用该ID查找用户名,并将其显示在UI中的每个单独post中。

首先,我们需要更新postAdded action creator以接受user的ID作为参数,并将其包含在action中。 (我们还将在initialState中更新现有的post条目,使其具有带有示例user的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从store中读取users列表,并将其显示为下拉列表。 然后,我们将获取所选user的ID,并将其传递给postAdded action creator。在此过程中,我们可以在表单中添加一些验证逻辑,以便user仅在标题和内容输入中包含一些实际文本的情况下才能单击“保存帖子”按钮:

// features/posts/AddPostForm.js
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>
  )
}

现在,我们需要一种在posts列表项和<SinglePostPage>中显示post的作者姓名的方法。由于我们想在多个地方显示相同的信息,因此我们可以制作一个PostAuthor组件,该组件以user的ID作为props,查找正确的user对象,并格式化用户名:

// features/posts/PostAuthor.js
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 store中读取数据的组件都可以使用useSelector hook,并提取其所需的特定数据。同样,许多组件可以同时访问Redux store中的相同数据。

现在,我们可以将PostAuthor组件导入到PostsList.jsSinglePostPage.js中,并将其呈现为<PostAuthor userId={post.user} />,并且每次添加post条目时,所选user的名称应显示在post中。

更多帖子功能

此时,我们可以创建和编辑posts。让我们添加一些其他逻辑,以使我们的posts feed更加有用。

存储posts的日期

社交媒体feed通常按post创建的时间进行排序,并以相对描述的形式向我们显示post的创建的时间,例如“5小时前”。为此,我们需要开始跟踪post条目的日期字段。

post.user字段一样,我们将更新postAdded的prepare callback,以确保在dispatch action时始终包含post.date。但是,这不是要传入的另一个参数。我们希望始终使用dispatch action时的确切时间戳,因此,我们让prepare callback本身对其进行处理。

警告

Redux action和state应仅包含普通JS值,例如对象,数组和值类型。不要将类实例,函数或其他不可序列化的值放入Redux!。

由于我们不能将Date类实例放入Redux store中,因此我们将post.date值作为时间戳字符串进行跟踪:

// features/posts/postsSlice.js
    postAdded: {
      reducer(state, action) {
        state.push(action.payload)
      },
      prepare(title, content, userId) {
        return {
          payload: {
            id: nanoid(),
            date: new Date().toISOString(),
            title,
            content,
            user: userId,
          },
        }
      },
    },

与post作者一样,我们需要在<PostsList><SinglePostPage>组件中显示相对时间戳说明。我们将添加一个<TimeAgo>组件来处理格式化时间戳字符串的相对描述。诸如date-fns之类的库具有一些有用的实用程序功能,用于解析和格式化日期,我们可以在这里使用它们:

// features/posts/TimeAgo.js
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}>
      &nbsp; <i>{timeAgo}</i>
    </span>
  )
}

对posts列表进行排序

当前,我们的<PostsList>以所有帖子在Redux store中保存的顺序显示所有posts。我们的示例首先显示了最早的post,并且每次添加新post时,它都会添加到posts数组的末尾。这意味着最新的post始终在页面底部。

通常,社交媒体feed首先显示最新的posts,然后向下滚动以查看较旧的posts。即使数据在store中保持最旧的优先,我们也可以在<PostsList>组件中对数据进行重新排序,以使最新的消息优先。从理论上讲,由于我们知道state.posts数组已经排序,因此我们可以反转列表。但是,最好还是自己进行排序以便确定。

由于array.sort()会更改现有数组,因此我们需要制作state.posts的副本并对该副本进行排序。我们知道我们的post.date字段被保留为日期时间戳字符串,我们可以直接比较它们以正确的顺序对posts进行排序:

// features/posts/PostsList.js
// 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从当前日期/时间减去分钟,以便它们彼此不同。

// features/posts/postsSlice.js
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()
  }
]

Post评价按钮

我们为此部分添加了另一个新功能。现在,我们的posts有点无聊。我们需要让他们更加激动,还有什么比让我们的朋友在我们的posts中添加评价表情符号更好的方法呢?

我们将在<PostsList><SinglePostPage>中每个帖子的底部添加一行表情符号评价按钮。每次用户单击评价按钮之一时,我们都需要在Redux store中为该post更新匹配的计数器字段。由于评价计数器数据位于Redux store中,因此在应用的不同部分之间进行切换应在使用该数据的任何组件中始终显示相同的值。

与post作者和时间戳一样,我们希望在显示post的任何地方都使用它,因此我们将创建一个以post为道具的<ReactionButtons>组件。我们首先显示内部的按钮以及每个按钮的当前评价计数:

// features/posts/ReactionButtons.js
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 post对象和postAdded prepare callback函数,以确保每个post都具有该数据,例如评价:reactions: {thumbsUp: 0, hooray: 0}

现在,我们可以定义一个新的reducer,当用户单击“评价”按钮时,它将处理更新post的评价计数。

与编辑post一样,我们需要知道post的ID,以及用户单击了哪个评价按钮。我们将有一个action.payload是一个看起来像{id,reaction}的对象。 然后,reducer可以找到正确的post对象,并更新正确的评价字段。

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进行将这段代码转换为安全的不可变更来执行。

请注意,我们的action对象仅包含描述所发生情况所需的最少信息。我们知道我们需要更新哪个post,以及单击了哪个评价名称。我们可以计算出新的评价计数器值并将其放入动作中,但始终最好使action对象尽可能小,并在reducer中进行state更新计算。这也意味着reducer可以包含计算新state所需的尽可能多的逻辑。

信息

使用Immer时,你可以“变更”现有的state对象,也可以自己返回新的state值,但不能同时返回两者。有关更多详细信息,请参见有关陷阱返回新数据的Immer文档指南。

我们的最后一步是更新<ReactionButtons>组件,以在用户单击按钮时dispatch responseAdded action:

// features/posts/ReactionButtons.jsx
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>也能看到更新后的数据。