Redux 要点,第 4 部分:使用 Redux 数据

👁️ 8626 ❤️ 633
Redux 要点,第 4 部分:使用 Redux 数据

Redux 要点,第 4 部分:使用 Redux 数据你将学到什么

在多个 React 组件中使用 Redux 数据

¥Using Redux data in multiple React components

组织调度动作的逻辑

¥Organizing logic that dispatches actions

使用选择器查找状态值

¥Using selectors to look up state values

在 reducer 中编写更复杂的更新逻辑

¥Writing more complex update logic in reducers

如何考虑 Redux 操作

¥How to think about Redux actions

先决条件

了解 第 3 部分中的 Redux 数据流和 React-Redux API

¥Understanding the Redux data flow and React-Redux APIs from Part 3

熟悉 用于页面路由的 React Router 组件

¥Familiarity with the React Router and components for page routing

介绍​

¥Introduction

在 第 3 部分:基本 Redux 数据流 中,我们了解了如何从空的 Redux+React 项目设置开始,添加新的状态片,并创建可以从 Redux 存储读取数据并分派操作来更新该数据的 React 组件。我们还研究了数据如何流经应用,其中组件调度操作、Reducer 处理操作并返回新状态,以及组件读取新状态并重新渲染 UI。我们还了解了如何创建 useSelector 和 useDispatch 钩子的 "pre-typed" 版本,这些钩子会自动应用正确的存储类型。

¥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. We also saw how to create "pre-typed" versions of the useSelector and useDispatch hooks that have the correct store types applied automatically.

现在你已经了解了编写 Redux 逻辑的核心步骤,我们将使用这些相同的步骤向我们的社交媒体源添加一些新功能,使其更加有用:查看单个帖子,编辑现有帖子,显示帖子作者详细信息、帖子时间戳、反应按钮和身份验证。

¥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, reaction buttons, and auth.

信息提醒一下,代码示例重点关注每个部分的关键概念和更改。请参阅 CodeSandbox 项目和 项目仓库中的 tutorial-steps-ts 分支 以了解应用中的完整更改。

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

显示单个帖子​

¥Showing Single Posts

由于我们能够向 Redux 存储添加新帖子,因此我们可以添加更多以不同方式使用帖子数据的功能。

¥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.

目前,我们的帖子条目显示在主提要页面中,但如果文本太长,我们只会显示内容的摘录。如果能够在自己的页面上查看单个帖子条目,将会很有帮助。

¥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.

创建单个帖子页面​

¥Creating a Single Post Page

首先,我们需要将新的 SinglePostPage 组件添加到 posts 功能文件夹中。当页面 URL 类似于 /posts/123 时,我们将使用 React Router 来显示此组件,其中 123 部分应该是我们要显示的帖子的 ID。

¥First, we need to add a new SinglePostPage component to our posts feature 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.

features/posts/SinglePostPage.tsximport { useParams } from 'react-router-dom'import { useAppSelector } from '@/app/hooks'export const SinglePostPage = () => { const { postId } = useParams() const post = useAppSelector(state => state.posts.find(post => post.id === postId) ) if (!post) { return (

Post not found!

) } return (

{post.title}

{post.content}

)}

当我们设置路由以渲染此组件时,我们将告诉它将 URL 的第二部分解析为名为 postId 的变量,我们可以从 useParams 钩子中读取该值。

¥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 the useParams hook.

一旦我们有了 postId 值,我们就可以在选择器函数中使用它来从 Redux 存储中找到正确的 post 对象。我们知道 state.posts 应该是所有帖子对象的数组,因此我们可以使用 Array.find() 函数循环遍历该数组并返回具有我们要查找的 ID 的帖子条目。

¥Once we have that postId value, we can use it inside a selector function to find the right post object from the Redux store. We know that state.posts should 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.

需要注意的是,每当从 useAppSelector 返回的值更改为新引用时,组件都会重新渲染。组件应始终尝试从存储中选择所需的尽可能少的数据,这将有助于确保它仅在实际需要时渲染。

¥It's important to note that the component will re-render any time the value returned from useAppSelector 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.

我们的存储中可能没有匹配的帖子条目 - 也许用户尝试直接输入 URL,或者我们没有加载正确的数据。如果发生这种情况,find() 函数将返回 undefined 而不是实际的 post 对象。我们的组件需要检查这一点并通过在页面中显示 "帖子未找到!" 消息来处理它。

¥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.

假设我们在存储中有正确的帖子对象,useAppSelector 将返回该对象,我们可以使用它在页面中渲染帖子的标题和内容。

¥Assuming we do have the right post object in the store, useAppSelector will return that, and we can use it to render the title and content of the post in the page.

你可能会注意到,这看起来与 组件主体中的逻辑非常相似,我们在整个 posts 数组上循环以显示主提要上的帖子摘录。我们可以尝试提取可在两个地方使用的 Post 组件,但我们显示帖子摘录和整个帖子的方式已经存在一些差异。即使存在一些重复,通常最好还是继续单独编写一段时间,然后我们可以稍后决定不同的代码部分是否足够相似,以至于我们可以真正提取可重用的组件。

¥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 on 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.

添加单个帖子路由​

¥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.

在我们这样做的同时,也值得将 "主页" 内容提取到单独的 组件中,只是为了可读性。

¥While we're at it, it's also worth extracting the "main page" content into a separate component as well, just for readability.

我们将在 App.tsx 中导入 PostsMainPage 和 SinglePostPage,并添加路由:

¥We'll import PostsMainPage and SinglePostPage in App.tsx, and add the route:

App.tsximport { BrowserRouter as Router, Route, Routes } from 'react-router-dom'import { Navbar } from './components/Navbar'import { PostsMainPage } from './features/posts/PostsMainPage'import { SinglePostPage } from './features/posts/SinglePostPage'function App() { return (

}> } />
)}export default App

然后,在 中,我们将更新列表渲染逻辑以包含路由到该特定帖子的

¥Then, in , we'll update the list rendering logic to include a that routes to that specific post:

features/posts/PostsList.tsximport { Link } from 'react-router-dom'import { useAppSelector } from '@/app/hooks'export const PostsList = () => { const posts = useAppSelector(state => state.posts) const renderedPosts = posts.map(post => (

{post.title}

{post.content.substring(0, 100)}

)) return (

Posts

{renderedPosts}
)}

由于我们现在可以点击进入不同的页面,因此在 组件中提供返回主帖子页面的链接也会很有帮助:

¥And since we can now click through to a different page, it would also be helpful to have a link back to the main posts page in the component as well:

app/Navbar.tsximport { Link } from 'react-router-dom'export const Navbar = () => { return (

)}

编辑帖子​

¥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.

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

¥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.

更新帖子条目​

¥Updating Post Entries

首先,我们需要更新 postsSlice 以创建新的 reducer 函数和操作,以便存储知道如何实际更新帖子。

¥First, we need to update our postsSlice to create a new reducer function and an action so that the store knows how to actually update posts.

在 createSlice() 调用内部,我们应该向 reducers 对象添加一个新函数。请记住,该 reducer 的名称应该能够很好地描述所发生的情况,因为每当调度此操作时,我们都会看到 reducer 名称作为操作类型字符串的一部分显示在 Redux DevTools 中。我们的第一个 reducer 称为 postAdded,所以我们将其称为 postUpdated。

¥Inside of the createSlice() call, we should add a new function into the reducers object. 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.

提示Redux 本身并不关心你对这些 Reducer 函数使用什么名称 - 如果它被命名为 postAdded、addPost、POST_ADDED 或 someRandomName,它将以相同的方式运行。

¥Redux itself doesn't care what name you use for these reducer functions - it'll run the same if it's named postAdded, addPost, POST_ADDED, or someRandomName.

也就是说,我们鼓励将 Reducer 命名为过去时 "发生这种情况" 名称,如 postAdded,因为我们正在描述 "应用中发生的事件"。

¥That said, we encourage naming reducers as past-tense "this happened" names like postAdded, because we're describing "an event that occurred in the application".

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

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

正在更新的帖子的 ID,以便我们可以在状态中找到正确的帖子对象

¥The ID of the post being updated, so that we can find the right post object in the state

用户输入的新 title 和 content 字段

¥The new title and content fields that the user typed in

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

¥Redux action objects are required to have a type field, 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}}.

默认情况下,createSlice 生成的操作创建者希望你传入一个参数,并且该值将作为 action.payload 放入操作对象中。因此,我们可以将包含这些字段的对象作为参数传递给 postUpdated 操作创建者。与 postAdded 一样,这是一个完整的 Post 对象,因此我们声明 Reducer 参数是 action: PayloadAction

¥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 postUpdated action creator. As with postAdded, this is an entire Post object, so we declare that the reducer argument is action: PayloadAction.

我们还知道,reducer 负责确定在分派操作时实际应如何更新状态。鉴于此,我们应该让 reducer 根据 ID 找到正确的 post 对象,并专门更新该 post 中的 title 和 content 字段。

¥We also know that the reducer is responsible for determining 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 title and content fields in that post.

最后,我们需要导出 createSlice 为我们生成的操作创建器函数,以便 UI 可以在用户保存帖子时调度新的 postUpdated 操作。

¥Finally, we'll need to export the action creator function that createSlice generated for us, so that the UI can dispatch the new postUpdated action when the user saves the post.

考虑到所有这些要求,完成后我们的 postsSlice 定义应如下所示:

¥Given all those requirements, here's how our postsSlice definition should look after we're done:

features/posts/postsSlice.tsimport { createSlice, PayloadAction } from '@reduxjs/toolkit'// omit state typesconst postsSlice = createSlice({ name: 'posts', initialState, reducers: { postAdded(state, action: PayloadAction) { state.push(action.payload) }, postUpdated(state, action: PayloadAction) { 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.actionsexport default postsSlice.reducer

创建编辑帖子表单​

¥Creating an Edit Post Form

我们的新 组件看起来与 相似,但逻辑需要略有不同。我们需要根据 URL 中的 postId 从存储中检索正确的 post 对象,然后使用它来初始化组件中的输入字段,以便用户可以进行更改。当用户提交表单时,我们会将更改后的标题和内容值保存回存储。我们还将使用 React Router 的 useNavigate 钩子切换到单个帖子页面,并在他们保存更改后显示该帖子。

¥Our new component will look similar to both the the and , but the logic needs to be a bit different. We need to retrieve the right post object from the store based on the postId in the URL, then use that to initialize the input fields in the component so the user can make changes. We'll save the changed title and content values back to the store when the user submits the form. We'll also use React Router's useNavigate hook to switch over to the single post page and show that post after they save the changes.

features/posts/EditPostForm.tsximport React from 'react'import { useNavigate, useParams } from 'react-router-dom'import { useAppSelector, useAppDispatch } from '@/app/hooks'import { postUpdated } from './postsSlice'// omit form element typesexport const EditPostForm = () => { const { postId } = useParams() const post = useAppSelector(state => state.posts.find(post => post.id === postId) ) const dispatch = useAppDispatch() const navigate = useNavigate() if (!post) { return (

Post not found!

) } const onSavePostClicked = (e: React.FormEvent) => { // Prevent server submission e.preventDefault() const { elements } = e.currentTarget const title = elements.postTitle.value const content = elements.postContent.value if (title && content) { dispatch(postUpdated({ id: post.id, title, content })) navigate(`/posts/${postId}`) } } return (

Edit Post