WHAT YOU’LL LEARN
  • Using Redux data in multiple React components
  • Organizing logic that dispatches actions
  • Writing more complex update logic in reducers

你将学会什么

  • 在多个React组件中使用Redux数据
  • 派发actions的组织逻辑
  • 在reducers中编写更复杂的更新逻辑

PREREQUISITES

前情提要

  • 从第三部分理解Redux数据流和React-Redux APIs
  • 熟悉页面路由React Router 组件

Introduction

In Part 3: Basic Redux Data Flow, we saw how to start from an empty Redux+React project setup, add a new slice of state, and create React components that can read data from the Redux store and dispatch actions to update that data. We also looked at how data flows through the application, with components dispatching actions, reducers processing actions and returning new state, and components reading the new state and rerendering the UI.
在part3:基础Redux数据流,我们看到从一个空Redux+React项目如何开始设置,增加一个新的状态slice,并创建可以从Redux store读取数据和派发actions去更新数据的React组件。我们也看数据流如何通过应用程序,和组件派发actions,reducers处理actions并返回新state,组件读取新state并渲染UI。

Now that you know the core steps to write Redux logic, we’re going to use those same steps to add some new features to our social media feed that will make it more useful: viewing a single post, editing existing posts, showing post author details, post timestamps, and reaction buttons.
现在你知道编写Redux逻辑的核心步骤,我们将使用这些相同步骤增加一些新功能给我们的社会化媒体feed,将使它更有用:查看单个帖子,编辑现有帖子,展示帖子作者详情,帖子时间戳,和互动按钮。

INFO

As a reminder, the code examples focus on the key concepts and changes for each section. See the CodeSandbox projects and the tutorial-steps branch in the project repo for the complete changes in the application.

信息 作为一个提醒,代码展示聚焦在关键概念和每个部分的变化。看CodeSandbox项目和项目仓库的教程步骤分支,完成应用程序改变。

Showing Single Posts

Since we have the ability to add new posts to the Redux store, we can add some more features that use the post data in different ways.
因为我们有能力增加新帖子给Redux store,我们可以增加更多功能,在不同方式中使用帖子数据。

Currently, our post entries are being shown in the main feed page, but if the text is too long, we only show an excerpt of the content. It would be helpful to have the ability to view a single post entry on its own page.
目前,我们的帖子条目是展示在主feed页面,但如果text超级长,我们仅仅展示内容的摘录,有能力去展示单个帖子条目在它自己的页面将很有帮助。

Creating a Single Post Page

First, we need to add a new SinglePostPagecomponent to our postsfeature folder. We’ll use React Router to show this component when the page URL looks like /posts/123, where the 123 part should be the ID of the post we want to show.
首先,我们需要增加新的SinglePostPage组件给我们的post 功能文件夹,当页面URL看起来像/post/123的时候我们将使用React Router 展示这个组件,123部分应该是我们想要展示的帖子ID。

features/posts/SinglePostPage.js

  1. import React from 'react'
  2. import { useSelector } from 'react-redux'
  3. export const SinglePostPage = ({ match }) => {
  4. const { postId } = match.params
  5. const post = useSelector(state =>
  6. state.posts.find(post => post.id === postId)
  7. )
  8. if (!post) {
  9. return (
  10. <section>
  11. <h2>Post not found!</h2>
  12. </section>
  13. )
  14. }
  15. return (
  16. <section>
  17. <article className="post">
  18. <h2>{post.title}</h2>
  19. <p className="post-content">{post.content}</p>
  20. </article>
  21. </section>
  22. )
  23. }

React Router will pass in a matchobject as a prop that contains the URL information we’re looking for. When we set up the route to render this component, we’re going to tell it to parse the second part of the URL as a variable named postId, and we can read that value from match.params.
React Router将传入一个match对象作为参数,它包含我们寻找的URL信息。当我们设置route渲染这个组件,我们将告诉它去解析URL的第二部分作为变量postId,并且我们可以通过match.params读取值。

Once we have that postIdvalue, we can use it inside a selector function to find the right post object from the Redux store. We know that state.postsshould be an array of all post objects, so we can use the Array.find() function to loop through the array and return the post entry with the ID we’re looking for.
一旦我们有了postId的值,我们可以在selector函数里面使用它,在Redux store中找到正确的post对象。我们知道state.posts应该是所有帖子对象的数组,所以我们使用Array.find()函数依次循环数组,并返回带有我们查找的ID的帖子条目。

It’s important to note that the component will re-render any time the value returned from useSelector changes to a new reference. Components should always try to select the smallest possible amount of data they need from the store, which will help ensure that it only renders when it actually needs to.
请务必注意,每当useSelector返回的值更改为新引用时,该组件都会重新渲染。组件应该始终尝试从store选择所需的最小数据量,这样将确保仅在实际需要的时候才进行渲染。

It’s possible that we might not have a matching post entry in the store - maybe the user tried to type in the URL directly, or we don’t have the right data loaded. If that happens, the find() function will return undefined instead of an actual post object. Our component needs to check for that and handle it by showing a “Post not found!” message in the page.
store中可能没有匹配的帖子条目-也许用户试图直接输入URL,或者我们没用正确的数据加载。如果发生了,find()函数将返回undefined代替实际的帖子对象。我们的组件需要检查并处理,在页面展示一个“Post not found!”的信息。

Assuming we do have the right post object in the store, useSelector will return that, and we can use it to render the title and content of the post in the page.
假设我们在store中没有正确的帖子对象,useSelector将返回那些,我们能用它来渲染页面的标题和内容。

You might notice that this looks fairly similar to the logic we have in the body of our component, where we loop over the whole posts array to show post excerpts the main feed. We could try to extract a Post component that could be used in both places, but there are already some differences in how we’re showing a post excerpt and the whole post. It’s usually better to keep writing things separately for a while even if there’s some duplication, and then we can decide later if the different sections of code are similar enough that we can really extract a reusable component.
你可能注意到了,这个看起来类似 我们在组件的逻辑,我们循环完整个帖子数组来展示帖子摘要的主feed。我们应该提取能用在两个地方帖子组件,但是在如何展示帖子摘要和整个帖子已经有一些不同。通常最好保持一段时间独立写东西,即使有重复,然后我们可以稍后决定代码的不同部分是否足够相似,以至于我们可以真正提取出可重用的组件。

Adding the Single Post Route

Now that we have a component, we can define a route to show it, and add links to each post in the front page feed.
现在我们有一个组件,我们可以定义一个路由来展示它,在首页feed给每个帖子增加链接。

We’ll import SinglePostPage in App.js, and add the route:
我们在App.js引入SinglePostPage,并增加路由:

App.js

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

Editing Posts

As a user, it’s really annoying to finish writing a post, save it, and realize you made a mistake somewhere. Having the ability to edit a post after we created it would be useful.
作为用户,写帖子,保存它,并意识到你某个地方犯的的错误确实很烦人。有能力编辑创建好的帖子是非常有用的。

Let’s add a new component that has the ability to take an existing post ID, read that post from the store, lets the user edit the title and post content, and then save the changes to update the post in the store.
让我们增加一个新的组件,能携带已存在的帖子ID,从store读取帖子,让用户编辑标题和帖子内容,然后保存更改更新store的post。

Updating Post Entries

First, we need to update our postsSliceto create a new reducer function and an action so that the store knows how to actually update posts.
首先,我们需要更新postsSlice来创建一个新的reducer函数和一个action让store知道如何实际更新posts。

Inside of thecreateSlice() call, we should add a new function into the reducersobject. Remember that the name of this reducer should be a good description of what’s happening, because we’re going to see the reducer name show up as part of the action type string in the Redux DevTools whenever this action is dispatched. Our first reducer was called postAdded, so let’s call this one postUpdated.
在createSlice内部调用,我们增加一个新函数到reducers对象。记住reduser的名字应该是反生什么了的描述,因为在Redux DevTools中,当这个action被派发,我们将看reducer名字作为action type 字符串的一部分展示。我们第一个reducer叫postAdded,所以让我们叫这个为postUpdated

In order to update a post object, we need to know:

  • The ID of the post being updated, so that we can find the right post object in the state
  • The new title and content fields that the user typed in

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

  • 被更新帖子的ID,以至于我们在state中能找到正确的帖子对象
  • 用户输入新的标题和内容字段

Redux action objects are required to have a typefield, which is normally a descriptive string, and may also contain other fields with more information about what happened. By convention, we normally put the additional info in a field called action.payload, but it’s up to us to decide what the payload field contains - it could be a string, a number, an object, an array, or something else. In this case, since we have three pieces of information we need, let’s plan on having the payload field be an object with the three fields inside of it. That means the action object will look like {type: 'posts/postUpdated', payload: {id, title, content}}.
Redux action 对象被引入需要类型字段,通常是描述性字符,也可以包含关于发生什么的其他更多信息字段。根据惯例,我们通常在字段action.payload放额外的信息,但由我们决定有效载荷字段包含的内容,他可以是一个字符串,数字,对象,数组,或者其他。本例中,因为我们需要三条信息,让我们计划payload字段作为一个包含三个字段的对象。这意味着action object看起来像{type:'post/postUpdated',payload:{id,title,content}}

By default, the action creators generated by createSlice expect you to pass in one argument, and that value will be put into the action object as action.payload. So, we can pass an object containing those fields as the argument to the postUpdatedaction creator.
默认情况下,通过createSlice生成的action creatore预期你传入一个参数,并且这个值将放进action对象作为action.payload.所以,我们将一个包含这些字段的对象作为参数传给postUpdatedaction creator。

We also know that the reducer is responsible for determing how the state should actually be updated when an action is dispatched. Given that, we should have the reducer find the right post object based on the ID, and specifically update the text and content fields in that post.
我们知道,reducer是负责当action被派发时,如何确定实际更新状态。鉴此,我们应该让reducer基于ID找到正确帖子对象,并专门更新那个帖子中的文字和内容字段。

Finally, we’ll need to export the action creator function that createSlice generated for us, so that the UI can dispatch the new postUpdatedaction when the user saves the post.
最终,我们需要导出createSlice生成的action creator函数,以至于当用户保存帖子时,UI可以dispatch新的**postUpdated** action。

Given all those requirements, here’s how our postsSlice definition should look after we’re done:
鉴于所有这些要求,在这里我们完成了我们的postsSlice如何定义:
features/posts/postsSlice.js

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

**

Creating an Edit Post Form

Our new component will look similar to the , but the logic needs to be a bit different. We need to retrieve the right post object from the store, then use that to initialize the state fields in the component so the user can make changes. We’ll save the changed title and content values back to the store after the user is done. We’ll also use React Router’s history API to switch over to the single post page and show that post.
我们新组件看起来像,但逻辑需要有所不同。我们需要从store找回正确的帖子对象,然后用组建中初始化状态字段以便用户可以修改。用户操作完我们将保存修改过的标题和内容返回给store。 我们也将用React Router的historyAPI去切换单独的post页面和展示post。
features/posts/EditPostForm.js

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

Like with SinglePostPage, we’ll need to import it into App.js and add a route that will render this component. We should also add a new link to our SinglePostPage that will route to EditPostPage, like:
像SinglePostPage一样,我们需要引入它到App.js并增加一个路由以便渲染这个组件。我们也应该增加一个新的链接到我们的SinglePostPage将路由到EditPostPage,像这样:
features/post/SinglePostPage.js

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>

Preparing Action Payloads

We just saw that the action creators from createSlicenormally expect one argument, which becomes action.payload. This simplifies the most common usage pattern, but sometimes we need to do more work to prepare the contents of an action object. In the case of our postAddedaction, we need to generate a unique ID for the new post, and we also need to make sure that the payload is an object that looks like {id, title, content}.
我们刚刚从createSlice看到action creators通常期望一个参数,变成action.payload。这个简化了最常用的使用模式,但有时候我们需要做更多工作比较action 对象的内容。对于我们的postAddedaction,我们需要为新的帖子生成一个唯一ID,我们也需要确定payload是一个类似{id, title, content}的对象。

Right now, we’re generating the ID and creating the payload object in our React component, and passing the payload object into postAdded. But, what if we needed to dispatch the same action from different components, or the logic for preparing the payload is complicated? We’d have to duplicate that logic every time we wanted to dispatch the action, and we’re forcing the component to know exactly what the payload for this action should look like.
现在,我们在React组件生成ID并创建payload对象,并传递payload对象给postAdded。但是,我们需要什么东西从不同组件派发相同action,或者准备payload的逻辑很复杂?每次我们想要dispatch action,不得不重复逻辑,我们强迫组件知道究竟payload action应该像什么样。

CAUTION

If an action needs to contain a unique ID or some other random value, always generate that first and put it in the action object. Reducers should never calculate random values, because that makes the results unpredictable.

警告 如果action需要包含唯一ID或者一些其他随机值,总是先生成然后放入action对象中。Reducers从不计算随机值,因为让结果不可预测。

If we were writing the postAddedaction creator by hand, we could have put the setup logic inside of it ourselves:
如果我们手写postAdded的action creator,我们自己将设置逻辑放入其中:

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

But, Redux Toolkit’s createSliceis generating these action creators for us. That makes the code shorter because we don’t have to write them ourselves, but we still need a way to customize the contents ofaction.payload.
但是,Redux Toolkit的 createSlice为我们生成这些action creator。那让代码更短,因为我们不需要我们自己写它们,但是我们仍然需要一种途径自定义action.payload的内容。

Fortunately, createSlicelets us define a “prepare callback” function when we write a reducer. The “prepare callback” function can take multiple arguments, generate random values like unique IDs, and run whatever other synchronous logic is needed to decide what values go into the action object. It should then return an object with the payloadfield inside. (The return object may also contain a metafield, which can be used to add extra descriptive values to the action, and an error field, which should be a boolean indicating whether this action represents some kind of an error.)
幸好,createSlice让我们在写reducer的时候定义一个“prepare回调”函数。“prepare回调”函数能携带多种参数,生成随机值比如唯一IDs,并运行其他需要的同步逻辑决定什么值走进action 对象。 他应该然后返回一个携带payload字段的对象。(返回对象也包含一个meta字段,这能被用在给action增加额外描述性值,和error字段,应该成为一个布尔指示是否action代表某种错误。)

Inside of the reducersfield in createSlice, we can define one of the fields as an object that looks like{reducer, prepare}:
代替createSlice中reducers字段,我们能定一个字段作为一个对象,比如{reducer, prepare}
features/posts/postsSlice.js

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

Now our component doesn’t have to worry about what the payload object looks like - the action creator will take care of putting it together the right way. So, we can update the component so that it passes in titleand contentas arguments when it dispatches postAdded:
现在我们的组件不需要担心payload对象看起来像什么-action creator将会注意正确方式放置到一起。所以,我们能更新组件,以便当它派发postAdded时传入title和content作为参数:

features/posts/AddPostForm.js

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

Users and Posts

So far, we only have one slice of state. The logic is defined in postsSlice.js, the data is stored in state.posts, and all of our components have been related to the posts feature. Real applications will probably have many different slices of state, and several different “feature folders” for the Redux logic and React components.
迄今,我们仅仅有一个状态的切片,在postsSlice.js中逻辑被定义,数据被存储在state.posts,我们的所有组件都与帖子功能有关。真实的应用程序或许有许多不同的状态切片,Redux逻辑的几个不同的“功能目录”和React组件。
You can’t have a “social media” app if there aren’t any other people involved. Let’s add the ability to keep track of a list of users in our app, and update the post-related functionality to make use of that data.
如果没有涉及其他人,你不能有一个“社会媒体”应用。让我们增加一种功能跟踪程序中的用户列表,并更新帖子相关功能以及利用该数据。

Adding a Users Slice

Since the concept of “users” is different than the concept of “posts”, we want to keep the code and data for the users separated from the code and data for posts. We’ll add a new features/users folder, and put a usersSlice file in there. Like with the posts slice, for now we’ll add some initial entries so that we have data to work with.
因为“用户”的概念是不同于“帖子”的概念,我们想要保持用户的代码和数据于帖子的代码和数据分开,我们将增加新文件夹 features/users,并放一个usersSlice文件在这里。像posts slice一样,现在我们增加一些初始的条目以至于我们有数据去工作。

features/users/usersSlice.js

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

For now, we don’t need to actually update the data, so we’ll leave the reducers field as an empty object. (We’ll come back to this in a later section.)
现在,我们不需要实际更新数据,所以我们将作为一个空对象。(我们将在后面部分再次讨论)

As before, we’ll import the usersReducer into our store file and add it to the store setup:
像之前一这样,我们引入userReducer到store文件中并添加到store设置中:
app/store.js

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

Adding Authors for Posts

Every post in our app was written by one of our users, and every time we add a new post, we should keep track of which user wrote that post. In a real app, we’d have some sort of a state.currentUser field that keeps track of the current logged-in user, and use that information whenever they add a post.
应用程序中每个帖子由一个用户撰写,并且每次我们增加一个新帖,我们应该保持跟踪哪个用户写了帖子,我们有一些 state.currentUser字段跟踪当前记录作者,并在他们添加帖子时使用该信息。

To keep things simpler for this example, we’ll update our component so that we can select a user from a dropdown list, and we’ll include that user’s ID as part of the post. Once our post objects have a user ID in them, we can use that to look up the user’s name and show it in each individual post in the UI.
为了使案例更简单,我们将更新我们的组件以至于我们能从下拉列表中选择用户,并且我们将引入用户ID作为帖子的一部分,一旦我们的帖子对象有一个用户ID,我们能用它查看用户的名字并且在UI的每个帖子中显示。

First, we need to update our postAdded action creator to accept a user ID as an argument, and include that in the action. (We’ll also update the existing post entries in initialState to have a post.user field with one of the example user IDs.)
首先,我们需要更新我们的postAdded action creator 去接受一个用户ID作为参数,在action中并引入。(我们也更新在初始化数据中已存在的帖子条目去有一个post.user字段和案例)

features/posts/postsSlice.js

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

Now, in our , we can read the list of users from the store with useSelector and show them as a dropdown. We’ll then take the ID of the selected user and pass that to the postAdded action creator. While we’re at it, we can add a bit of validation logic to our form so that the user can only click the “Save Post” button if the title and content inputs have some actual text in them:
现在,在组件中,我们能使用useSelector从store中读取用户列表,并且作为下拉展示他们。我们将携带已选用户的ID并传递给PostAdded action creator.当我们到这儿时,我们能添加一个验证逻辑给我们的表单,如果title和content的input框有实际内容,用户才能点击“Save Post”按钮。

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

Now, we need a way to show the name of the post’s author inside of our post list items and <SinglePostPage>. Since we want to show this same kind of info in more than one place, we can make a PostAuthorcomponent that takes a user ID as a prop, looks up the right user object, and formats the user’s name:
现在,我们需要一种方式展示帖子作者的名字代替帖子列表项。因为我们想要展示这些相同种类信息在多个地方,我也可以创建PostAutor组件携带用户ID作为prop,查询正确用户对象,并格式化用户名:
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>
}

Notice that we’re following the same pattern in each of our components as we go. Any component that needs to read data from the Redux store can use the useSelectorhook, and extract the specific pieces of data that it needs. Also, many components can access the same data in the Redux store at the same time.
注意,我们每个组件都要遵循相同的模式。任何需要从Redux store读取数据的组件可以使用useSelector 钩子,并提取它具体需要的数据pieces。同样,很多组件能同时访问在Redux store中的相同数据。

We can now import the PostAuthor component into both PostsList.js and SinglePostPage.js, and render it as , and every time we add a post entry, the selected user’s name should show up inside of the rendered post.
我们现在能引入PostAuthor组件到PostsList.js和SinglePostPage.js,并用渲染,每次我们增加帖子条目,选择用户名应呈现在渲染的帖子里面。

More Post Features

At this point, we can create and edit posts. Let’s add some additional logic to make our posts feed more useful.
至此,我们能创建和编辑帖子,让我们增加一些额外逻辑到使我们的帖子feed更有用。

Sorting the Posts List

Social media feeds are typically sorted by when the post was created, and show us the post creation time as a relative description like “5 hours ago”. In order to do that, we need to start tracking a date field for our post entries.

Post Reaction Buttons

What You’ve Learned

What’s Next?