Skip to main content

Redux 基础知识,第 5 部分:UI 和 React

你将学到什么
  • Redux 存储如何与 UI 配合使用

    ¥How a Redux store works with a UI

  • 如何将 Redux 与 React 结合使用

    ¥How to use Redux with React

介绍

¥Introduction

第 4 部分:存储 中,我们了解了如何创建 Redux 存储、调度操作以及读取当前状态。我们还研究了存储在内部如何工作,增强器和中间件如何让我们使用附加功能自定义存储,以及如何添加 Redux DevTools 以便让我们在分派操作时查看应用内部发生的情况。

¥In Part 4: Store, we saw how to create a Redux store, dispatch actions, and read the current state. We also looked at how a store works inside, how enhancers and middleware let us customize the store with additional abilities, and how to add the Redux DevTools to let us see what's happening inside our app as actions are dispatched.

在本节中,我们将为我们的待办事项应用添加一个用户界面。我们将了解 Redux 如何与 UI 层一起工作,并且我们将具体介绍 Redux 如何与 React 一起工作。

¥In this section, we'll add a User Interface for our todo app. We'll see how Redux works with a UI layer overall, and we'll specifically cover how Redux works together with React.

提醒

请注意,本页和所有 "必需品" 教程都教授如何使用 我们现代的 React-Redux hooks API。旧式 connect API 仍然有效,但今天我们希望所有 Redux 用户都使用 hooks API。

¥Note that this page and all of the "Essentials" tutorial teach how to use our modern React-Redux hooks API. The old-style connect API still works, but today we want all Redux users using the hooks API.

此外,本教程的其他页面有意展示旧式 Redux 逻辑模式,这些模式需要比我们今天教授的 Redux Toolkit 中的 "现代回归" 模式更多的代码,作为使用 Redux 构建应用的正确方法,以解释 Redux 背后的原理和概念。

¥Also, the other pages in this tutorial intentionally show older-style Redux logic patterns that require more code than the "modern Redux" patterns with Redux Toolkit we teach as the right approach for building apps with Redux today, in order to explain the principles and concepts behind Redux.

请参阅 "Redux 要点" 教程,了解 "如何正确使用 Redux" 与 Redux Toolkit 和 React-Redux hooks 用于实际应用的完整示例。

¥See the "Redux Essentials" tutorial for full examples of "how to use Redux, the right way" with Redux Toolkit and React-Redux hooks for real-world apps.

将 Redux 与 UI 集成

¥Integrating Redux with a UI

Redux 是一个独立的 JS 库。正如我们已经看到的,即使你没有设置用户界面,你也可以创建和使用 Redux 存储。这也意味着你可以将 Redux 与任何 UI 框架(甚至没有任何 UI 框架)一起使用,并在客户端和服务器上使用它。你可以使用 React、Vue、Angular、Ember、jQuery 或 vanilla JavaScript 编写 Redux 应用。

¥Redux is a standalone JS library. As we've already seen, you can create and use a Redux store even if you don't have a user interface set up. This also means that you can use Redux with any UI framework (or even without any UI framework), and use it on both client and server. You can write Redux apps with React, Vue, Angular, Ember, jQuery, or vanilla JavaScript.

也就是说,Redux 是专门为与 React 良好配合而设计的。React 允许你将 UI 描述为状态的函数,而 Redux 包含状态并根据操作更新它。

¥That said, Redux was specifically designed to work well with React. React lets you describe your UI as a function of your state, and Redux contains state and updates it in response to actions.

因此,在本教程中,我们将在构建待办事项应用时使用 React,并介绍如何将 React 与 Redux 结合使用的基础知识。

¥Because of that, we'll use React for this tutorial as we build our todo app, and cover the basics of how to use React with Redux.

在讨论这一部分之前,我们先快速了解一下 Redux 一般如何与 UI 层交互。

¥Before we get to that part, let's take a quick look at how Redux interacts with a UI layer in general.

基本 Redux 和 UI 集成

¥Basic Redux and UI Integration

将 Redux 与任何 UI 层一起使用都需要几个一致的步骤:

¥Using Redux with any UI layer requires a few consistent steps:

  1. 创建 Redux 存储

    ¥Create a Redux store

  2. 订阅更新

    ¥Subscribe to updates

  3. 订阅回调内部:

    ¥Inside the subscription callback:

    1. 获取当前存储状态

      ¥Get the current store state

    2. 提取这块 UI 需要的数据

      ¥Extract the data needed by this piece of UI

    3. 使用数据更新 UI

      ¥Update the UI with the data

  4. 如有必要,以初始状态渲染 UI

    ¥If necessary, render the UI with initial state

  5. 通过调度 Redux 操作来响应 UI 输入

    ¥Respond to UI inputs by dispatching Redux actions

让我们回到 我们在第 1 部分中看到的计数器应用示例,看看它是如何执行这些步骤的:

¥Let's go back to the the counter app example we saw in Part 1 and see how it follows those steps:

// 1) Create a new Redux store with the `createStore` function
const store = Redux.createStore(counterReducer)

// 2) Subscribe to redraw whenever the data changes in the future
store.subscribe(render)

// Our "user interface" is some text in a single HTML element
const valueEl = document.getElementById('value')

// 3) When the subscription callback runs:
function render() {
// 3.1) Get the current store state
const state = store.getState()
// 3.2) Extract the data you want
const newValue = state.value.toString()

// 3.3) Update the UI with the new value
valueEl.innerHTML = newValue
}

// 4) Display the UI with the initial store state
render()

// 5) Dispatch actions based on UI inputs
document.getElementById('increment').addEventListener('click', function () {
store.dispatch({ type: 'counter/incremented' })
})

无论你使用哪个 UI 层,Redux 都会以相同的方式处理每个 UI。实际的实现通常会更复杂一些,以帮助优化性能,但每次的步骤都是相同的。

¥No matter what UI layer you're using, Redux works this same way with every UI. The actual implementations are typically a bit more complicated to help optimize performance, but it's the same steps each time.

由于 Redux 是一个单独的库,因此有不同的 "binding" 库可以帮助你将 Redux 与给定的 UI 框架一起使用。这些 UI 绑定库处理订阅存储的详细信息,并在状态更改时有效更新 UI,因此你不必自己编写该代码。

¥Since Redux is a separate library, there are different "binding" libraries to help you use Redux with a given UI framework. Those UI binding libraries handle the details of subscribing to the store and efficiently updating the UI as state changes, so that you don't have to write that code yourself.

将 Redux 与 React 结合使用

¥Using Redux with React

官方的 React-Redux UI 绑定库 是与 Redux 核心分开的一个包。你还需要安装它:

¥The official React-Redux UI bindings library is a separate package from the Redux core. You'll need to install that in addition:

npm install react-redux

在本教程中,我们将介绍一起使用 React 和 Redux 所需的最重要的模式和示例,并了解它们作为我们的待办事项应用的一部分在实践中如何工作。

¥For this tutorial, we'll cover the most important patterns and examples you need to use React and Redux together, and see how they work in practice as part of our todo app.

信息

请参阅 https://react-redux.nodejs.cn 上的官方 React-Redux 文档,获取有关如何一起使用 Redux 和 React 的完整指南,以及有关 React-Redux API 的参考文档。

¥See the official React-Redux docs at https://react-redux.nodejs.cn for a complete guide on how to use Redux and React together, and reference documentation on the React-Redux APIs.

设计组件树

¥Designing the Component Tree

就像我们根据需求 设计的状态结构 一样,我们也可以设计整个 UI 组件集以及它们在应用中如何相互关联。

¥Much like we designed the state structure based on requirements, we can also design the overall set of UI components and how they relate to each other in the application.

基于 应用的业务需求列表,我们至少需要这组组件:

¥Based on the list of business requirements for the app, at a minimum we're going to need this set of components:

  • <App>:渲染其他所有内容的根组件。

    ¥<App>: the root component that renders everything else.

    • <Header>:包含 "新待办事项" 文本输入和 "完成所有待办事项" 复选框

      ¥<Header>: contains the "new todo" text input and the "complete all todos" checkbox

    • <TodoList>:基于过滤结果的所有当前可见待办事项的列表

      ¥<TodoList>: a list of all currently visible todo items, based on the filtered results

      • <TodoListItem>:单个待办事项列表项,带有一个可以单击以切换待办事项完成状态的复选框,以及一个颜色类别选择器

        ¥<TodoListItem>: a single todo list item, with a checkbox that can be clicked to toggle the todo's completed status, and a color category selector

    • <Footer>:显示活动待办事项的数量以及用于根据已完成状态和颜色类别过滤列表的控件的数量

      ¥<Footer>: Shows the number of active todos and controls for filtering the list based on completed status and color category

除了这个基本的组件结构之外,我们还可以用几种不同的方式来划分组件。例如,<Footer> 组件可能是一个较大的组件,也可能内部有多个较小的组件,如 <CompletedTodos><StatusFilter><ColorFilters>。没有单一的正确方法来划分这些组件,你会发现根据你的情况编写较大的组件或将其拆分为许多较小的组件可能会更好。

¥Beyond this basic component structure, we could potentially divide the components up in several different ways. For example, the <Footer> component could be one larger component, or it could have multiple smaller components inside like <CompletedTodos>, <StatusFilter>, and <ColorFilters>. There's no single right way to divide these, and you'll find that it may be better to write larger components or split things into many smaller components depending on your situation.

现在,我们将从这个小组件列表开始,以使事情更容易理解。在这一点上,由于我们假设 你已经知道 React,我们将跳过如何为这些组件编写布局代码的细节,并重点关注如何在 React 组件中实际使用 React-Redux 库。

¥For now, we'll start with this small list of components to keep things easier to follow. On that note, since we assume that you already know React, we're going to skip past the details of how to write the layout code for these components and focus on how to actually use the React-Redux library in your React components.

在我们开始添加任何 Redux 相关逻辑之前,这是该应用的初始 React UI:

¥Here's the initial React UI of this app before we start adding any Redux-related logic:

使用 useSelector 从 Store 读取状态

¥Reading State from the Store with useSelector

我们知道我们需要能够显示待办事项列表。我们首先创建一个 <TodoList> 组件,该组件可以从存储读取待办事项列表,循环遍历它们,并为每个待办事项条目显示一个 <TodoListItem> 组件。

¥We know that we need to be able to show a list of todo items. Let's start by creating a <TodoList> component that can read the list of todos from the store, loop over them, and show one <TodoListItem> component for each todo entry.

你应该熟悉 useState 这样的 React hooks,它可以在 React 函数组件中调用,以使其能够访问 React 状态值。React 还允许我们编写 定制钩子,它允许我们提取可重用的钩子,以便在 React 的内置钩子之上添加我们自己的行为。

¥You should be familiar with React hooks like useState, which can be called in React function components to give them access to React state values. React also lets us write custom hooks, which let us extract reusable hooks to add our own behavior on top of React's built-in hooks.

与许多其他库一样,React-Redux 包含 它自己的定制钩子,你可以在自己的组件中使用它。React-Redux 钩子使你的 React 组件能够通过读取状态和分派操作来与 Redux 存储进行通信。

¥Like many other libraries, React-Redux includes its own custom hooks, which you can use in your own components. The React-Redux hooks give your React component the ability to talk to the Redux store by reading state and dispatching actions.

我们要查看的第一个 React-Redux 钩子是 useSelector,它允许你的 React 组件从 Redux 存储中读取数据。

¥The first React-Redux hook that we'll look at is the useSelector hook, which lets your React components read data from the Redux store.

useSelector 接受单个函数,我们称之为选择器函数。选择器是一个函数,它将整个 Redux 存储状态作为参数,从状态中读取一些值,然后返回该结果。

¥useSelector accepts a single function, which we call a selector function. A selector is a function that takes the entire Redux store state as its argument, reads some value from the state, and returns that result.

例如,我们知道我们的待办事项应用的 Redux 状态将待办事项数组保留为 state.todos。我们可以编写一个小的选择器函数来返回 todos 数组:

¥For example, we know that our todo app's Redux state keeps the array of todo items as state.todos. We can write a small selector function that returns that todos array:

const selectTodos = state => state.todos

或者,也许我们想知道当前有多少待办事项标记为 "completed":

¥Or, maybe we want to find out how many todos are currently marked as "completed":

const selectTotalCompletedTodos = state => {
const completedTodos = state.todos.filter(todo => todo.completed)
return completedTodos.length
}

因此,选择器可以从 Redux 存储状态返回值,也可以返回基于该状态的派生值。

¥So, selectors can return values from the Redux store state, and also return derived values based on that state as well.

让我们将待办事项数组读入 <TodoList> 组件中。首先,我们将从 react-redux 库导入 useSelector 钩子,然后使用选择器函数作为参数来调用它:

¥Let's read the array of todos into our <TodoList> component. First, we'll import the useSelector hook from the react-redux library, then call it with a selector function as its argument:

src/features/todos/TodoList.js
import React from 'react'
import { useSelector } from 'react-redux'
import TodoListItem from './TodoListItem'

const selectTodos = state => state.todos

const TodoList = () => {
const todos = useSelector(selectTodos)

// since `todos` is an array, we can loop over it
const renderedListItems = todos.map(todo => {
return <TodoListItem key={todo.id} todo={todo} />
})

return <ul className="todo-list">{renderedListItems}</ul>
}

export default TodoList

<TodoList> 组件第一次渲染时,useSelector hook 将调用 selectTodos 并传入整个 Redux 状态对象。无论选择器返回什么,钩子都会将其返回到你的组件。因此,我们组件中的 const todos 最终将在 Redux 存储状态中保存相同的 state.todos 数组。

¥The first time the <TodoList> component renders, the useSelector hook will call selectTodos and pass in the entire Redux state object. Whatever the selector returns will be returned by the hook to your component. So, the const todos in our component will end up holding the same state.todos array inside our Redux store state.

但是,如果我们调度像 {type: 'todos/todoAdded'} 这样的操作会发生什么?Redux 状态将由 reducer 更新,但我们的组件需要知道某些内容发生了变化,以便它可以使用新的待办事项列表重新渲染。

¥But, what happens if we dispatch an action like {type: 'todos/todoAdded'}? The Redux state will be updated by the reducer, but our component needs to know that something has changed so that it can re-render with the new list of todos.

我们知道我们可以调用 store.subscribe() 来监听 store 的变化,所以我们可以尝试在每个组件中编写代码来订阅 store。但是,这很快就会变得非常重复并且难以处理。

¥We know that we can call store.subscribe() to listen for changes to the store, so we could try writing the code to subscribe to the store in every component. But, that would quickly get very repetitive and hard to handle.

幸运的是,useSelector 自动为我们订阅了 Redux 存储!这样,每当调度一个动作时,它都会立即再次调用它的选择器函数。如果选择器返回的值较上次运行时发生变化,useSelector 将强制我们的组件使用新数据重新渲染。我们所要做的就是在组件中调用 useSelector() 一次,它会为我们完成其余的工作。

¥Fortunately, useSelector automatically subscribes to the Redux store for us! That way, any time an action is dispatched, it will call its selector function again right away. If the value returned by the selector changes from the last time it ran, useSelector will force our component to re-render with the new data. All we have to do is call useSelector() once in our component, and it does the rest of the work for us.

然而,这里有一件非常重要的事情要记住:

¥However, there's a very important thing to remember here:

提醒

useSelector 使用严格的 === 引用比较来比较其结果,因此只要选择器结果是新引用,组件就会重新渲染!这意味着,如果你在选择器中创建一个新引用并返回它,则每次分派操作时,你的组件都可能会重新渲染,即使数据实际上没有不同。

¥useSelector compares its results using strict === reference comparisons, so the component will re-render any time the selector result is a new reference! This means that if you create a new reference in your selector and return it, your component could re-render every time an action has been dispatched, even if the data really isn't different.

例如,将此选择器传递给 useSelector 将导致组件始终重新渲染,因为 array.map() 始终返回新的数组引用:

¥For example, passing this selector to useSelector will cause the component to always re-render, because array.map() always returns a new array reference:

// Bad: always returning a new reference
const selectTodoDescriptions = state => {
// This creates a new array reference!
return state.todos.map(todo => todo.text)
}
提示

我们将在本节后面讨论解决此问题的一种方法。我们还将讨论如何使用 第 7 部分:标准 Redux 模式 中的 "memoized" 选择器功能来提高性能并避免不必要的重新渲染。

¥We'll talk about one way to fix this issue later in this section. We'll also talk about how you can improve performance and avoid unnecessary re-renders using "memoized" selector function in Part 7: Standard Redux Patterns.

还值得注意的是,我们不必将选择器函数编写为单独的变量。你可以直接在 useSelector 的调用中编写选择器函数,如下所示:

¥It's also worth noting that we don't have to write a selector function as a separate variable. You can write a selector function directly inside the call to useSelector, like this:

const todos = useSelector(state => state.todos)

使用 useDispatch 调度操作

¥Dispatching Actions with useDispatch

我们现在知道如何将数据从 Redux 存储读取到我们的组件中。但是,我们如何从组件将操作分派到存储?我们知道在 React 之外,我们可以调用 store.dispatch(action)。由于我们无权访问组件文件中的存储,因此我们需要某种方法来访问组件内部的 dispatch 函数本身。

¥We now know how to read data from the Redux store into our components. But, how can we dispatch actions to the store from a component? We know that outside of React, we can call store.dispatch(action). Since we don't have access to the store in a component file, we need some way to get access to the dispatch function by itself inside our components.

React-Redux useDispatch 为我们提供了存储的 dispatch 方法作为其结果。(事实上,hook 的实现确实是 return store.dispatch。)

¥The React-Redux useDispatch hook gives us the store's dispatch method as its result. (In fact, the implementation of the hook really is return store.dispatch.)

因此,我们可以在任何需要分派动作的组件中调用 const dispatch = useDispatch(),然后根据需要调用 dispatch(someAction)

¥So, we can call const dispatch = useDispatch() in any component that needs to dispatch actions, and then call dispatch(someAction) as needed.

让我们在 <Header> 组件中尝试一下。我们知道,我们需要让用户为新的待办事项输入一些文本,然后调度包含该文本的 {type: 'todos/todoAdded'} 操作。

¥Let's try that in our <Header> component. We know that we need to let the user type in some text for a new todo item, and then dispatch a {type: 'todos/todoAdded'} action containing that text.

我们将编写一个典型的 React 表单组件,它使用 "受控输入" 让用户输入表单文本。然后,当用户专门按下 Enter 键时,我们将调度该操作。

¥We'll write a typical React form component that uses "controlled inputs" to let the user type in the form text. Then, when the user presses the Enter key specifically, we'll dispatch that action.

src/features/header/Header.js
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'

const Header = () => {
const [text, setText] = useState('')
const dispatch = useDispatch()

const handleChange = e => setText(e.target.value)

const handleKeyDown = e => {
const trimmedText = e.target.value.trim()
// If the user pressed the Enter key:
if (e.key === 'Enter' && trimmedText) {
// Dispatch the "todo added" action with this text
dispatch({ type: 'todos/todoAdded', payload: trimmedText })
// And clear out the text input
setText('')
}
}

return (
<input
type="text"
placeholder="What needs to be done?"
autoFocus={true}
value={text}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
)
}

export default Header

Provider 一起经过存储

¥Passing the Store with Provider

我们的组件现在可以从存储读取状态,并将操作分派到存储。然而,我们仍然缺少一些东西。React-Redux 钩子在哪里以及如何找到正确的 Redux 存储?hook 是一个 JS 函数,因此它本身无法自动从 store.js 导入 store。

¥Our components can now read state from the store, and dispatch actions to the store. However, we're still missing something. Where and how are the React-Redux hooks finding the right Redux store? A hook is a JS function, so it can't automatically import a store from store.js by itself.

相反,我们必须明确告诉 React-Redux 我们想要在组件中使用什么存储。我们通过围绕整个 <App> 渲染 <Provider> 组件,并将 Redux 存储作为 props 传递给 <Provider> 来实现这一点。执行此操作一次后,应用中的每个组件都可以根据需要访问 Redux 存储。

¥Instead, we have to specifically tell React-Redux what store we want to use in our components. We do this by rendering a <Provider> component around our entire <App>, and passing the Redux store as a prop to <Provider>. After we do this once, every component in the application will be able to access the Redux store if it needs to.

让我们将其添加到我们的主 index.js 文件中:

¥Let's add that to our main index.js file:

src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'

import App from './App'
import store from './store'

ReactDOM.render(
// Render a `<Provider>` around the entire `<App>`,
// and pass the Redux store to it as a prop
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
)

这涵盖了将 React-Redux 与 React 结合使用的关键部分:

¥That covers the key parts of using React-Redux with React:

  • 调用 useSelector 钩子读取 React 组件中的数据

    ¥Call the useSelector hook to read data in React components

  • 调用 useDispatch 钩子来调度 React 组件中的操作

    ¥Call the useDispatch hook to dispatch actions in React components

  • <Provider store={store}> 放在整个 <App> 组件周围,以便其他组件可以与存储通信

    ¥Put <Provider store={store}> around your entire <App> component so that other components can talk to the store

我们现在应该能够与应用实际交互!这是到目前为止的工作用户界面:

¥We should now be able to actually interact with the app! Here's the working UI so far:

现在,让我们看看在我们的待办事项应用中一起使用这些功能的更多方法。

¥Now, let's look at a couple more ways we can use these together in our todo app.

React-Redux 模式

¥React-Redux Patterns

全局状态、组件状态和表单

¥Global State, Component State, and Forms

现在你可能想知道,"我是否总是需要将所有应用的状态放入 Redux 存储中?"

¥By now you might be wondering, "Do I always have to put all my app's state into the Redux store?"

答案是不。应用所需的全局状态应该放在 Redux 存储中。仅在一处需要的状态应保留在组件状态中。

¥The answer is NO. Global state that is needed across the app should go in the Redux store. State that's only needed in one place should be kept in component state.

我们之前编写的 <Header> 组件就是一个很好的例子。我们可以通过在输入的 onChange 处理程序中分派一个操作并将其保存在我们的化简器中,将当前文本输入字符串保留在 Redux 存储中。但是,这并没有给我们带来任何好处。唯一使用文本字符串的地方是此处,在 <Header> 组件中。

¥A good example of this is the <Header> component we wrote earlier. We could keep the current text input string in the Redux store, by dispatching an action in the input's onChange handler and keeping it in our reducer. But, that doesn't give us any benefit. The only place that text string is used is here, in the <Header> component.

因此,将该值保留在 <Header> 组件的 useState 钩子中是有意义的。

¥So, it makes sense to keep that value in a useState hook here in the <Header> component.

同样,如果我们有一个名为 isDropdownOpen 的布尔标志,应用中的其他组件不会关心它 - 它确实应该保留在该组件的本地。

¥Similarly, if we had a boolean flag called isDropdownOpen, no other components in the app would care about that - it should really stay local to this component.

提示

在 React + Redux 应用中,你的全局状态应位于 Redux 存储中,而本地状态应保留在 React 组件中。

¥In a React + Redux app, your global state should go in the Redux store, and your local state should stay in React components.

如果你不确定将某些内容放在哪里,可以使用以下一些常见的经验规则来确定应将哪种数据放入 Redux 中:

¥If you're not sure where to put something, here are some common rules of thumb for determining what kind of data should be put into Redux:

  • 应用的其他部分是否关心这些数据?

    ¥Do other parts of the application care about this data?

  • 你是否需要能够基于此原始数据创建进一步的派生数据?

    ¥Do you need to be able to create further derived data based on this original data?

  • 是否使用相同的数据来驱动多个组件?

    ¥Is the same data being used to drive multiple components?

  • 能够将此状态恢复到给定时间点(即时间旅行调试)对你有价值吗?

    ¥Is there value to you in being able to restore this state to a given point in time (ie, time travel debugging)?

  • 你是否想要缓存数据(即,如果状态已经存在,则使用状态而不是重新请求它)?

    ¥Do you want to cache the data (ie, use what's in state if it's already there instead of re-requesting it)?

  • 你是否希望在热重载 UI 组件时保持这些数据一致(交换时可能会丢失其内部状态)?

    ¥Do you want to keep this data consistent while hot-reloading UI components (which may lose their internal state when swapped)?

这也是一般如何思考 Redux 中表单的一个很好的例子。大多数表单状态可能不应该保存在 Redux 中。相反,在编辑数据时将数据保留在表单组件中,然后在用户完成后分派 Redux 操作来更新存储。

¥This is also a good example of how to think about forms in Redux in general. Most form state probably shouldn't be kept in Redux. Instead, keep the data in your form components as you're editing it, and then dispatch Redux actions to update the store when the user is done.

在组件中使用多个选择器

¥Using Multiple Selectors in a Component

现在只有我们的 <TodoList> 组件正在从存储中读取数据。让我们看看 <Footer> 组件开始读取一些数据时会是什么样子。

¥Right now only our <TodoList> component is reading data from the store. Let's see what it might look like for the <Footer> component to start reading some data as well.

<Footer> 需要知道三个不同的信息:

¥The <Footer> needs to know three different pieces of information:

  • 有多少已完成的待办事项

    ¥How many completed todos there are

  • 当前 "status" 过滤器值

    ¥The current "status" filter value

  • 当前选定的 "color" 类别过滤器列表

    ¥The current list of selected "color" category filters

我们如何将这些值读取到组件中?

¥How can we read these values into the component?

我们可以在一个组件内多次调用 useSelector。事实上,这确实是一个好主意 - 每次调用 useSelector 应始终返回尽可能少的状态。

¥We can call useSelector multiple times within one component. In fact, this is actually a good idea - each call to useSelector should always return the smallest amount of state possible.

我们之前已经了解了如何编写一个对已完成的待办事项进行计数的选择器。对于过滤器值,状态过滤器值和颜色过滤器值都位于 state.filters 切片中。由于该组件需要两者,因此我们可以选择整个 state.filters 对象。

¥We already saw how to write a selector that counts completed todos earlier. For the filters values, both the status filter value and the color filters values live in the state.filters slice. Since this component needs both of them, we can select the entire state.filters object.

正如我们之前提到的,我们可以将所有输入处理直接放入 <Footer> 中,或者我们可以将其拆分为单独的组件,例如 <StatusFilter>。为了使这个解释更简短,我们将跳过编写输入处理的具体细节,并假设我们有更小的单独组件,它们被赋予一些数据并更改处理程序回调作为属性。

¥As we mentioned earlier, we could put all the input handling directly into <Footer>, or we could split it out into separate components like <StatusFilter>. To keep this explanation shorter, we'll skip the exact details of writing the input handling and assume we've got smaller separate components that are given some data and change handler callbacks as props.

考虑到这一假设,组件的 React-Redux 部分可能如下所示:

¥Given that assumption, the React-Redux parts of the component might look like this:

src/features/footer/Footer.js
import React from 'react'
import { useSelector } from 'react-redux'

import { availableColors, capitalize } from '../filters/colors'
import { StatusFilters } from '../filters/filtersSlice'

// Omit other footer components

const Footer = () => {
const todosRemaining = useSelector(state => {
const uncompletedTodos = state.todos.filter(todo => !todo.completed)
return uncompletedTodos.length
})

const { status, colors } = useSelector(state => state.filters)

// omit placeholder change handlers

return (
<footer className="footer">
<div className="actions">
<h5>Actions</h5>
<button className="button">Mark All Completed</button>
<button className="button">Clear Completed</button>
</div>

<RemainingTodos count={todosRemaining} />
<StatusFilter value={status} onChange={onStatusChange} />
<ColorFilters value={colors} onChange={onColorChange} />
</footer>
)
}

export default Footer

按 ID 选择列表项中的数据

¥Selecting Data in List Items by ID

目前,我们的 <TodoList> 正在读取整个 state.todos 数组,并将实际的待办事项对象作为 prop 传递给每个 <TodoListItem> 组件。

¥Currently, our <TodoList> is reading the entire state.todos array and passing the actual todo objects as a prop to each <TodoListItem> component.

这可行,但存在潜在的性能问题。

¥This works, but there's a potential performance problem.

  • 更改一个 todo 对象意味着创建 todo 和 state.todos 数组的副本,每个副本都是内存中的一个新引用

    ¥Changing one todo object means creating copies of both the todo and the state.todos array, and each copy is a new reference in memory

  • useSelector 看到新的引用作为其结果时,它会强制其组件重新渲染

    ¥When useSelector sees a new reference as its result, it forces its component to re-render

  • 因此,任何时候更新一个待办事项对象(例如单击它以切换其完成状态),整个 <TodoList> 父组件都会重新渲染

    ¥So, any time one todo object is updated (like clicking it to toggle its completed status), the whole <TodoList> parent component will re-render

  • 然后,因为 React 默认会递归地重新渲染所有子组件,它也意味着所有 <TodoListItem> 组件将重新渲染,尽管其中大多数实际上根本没有改变!

    ¥Then, because React re-renders all child components recursively by default, it also means that all of the <TodoListItem> components will re-render, even though most of them didn't actually change at all!

重新渲染组件也不错 - 这就是 React 知道是否需要更新 DOM 的方式。但是,如果列表太大,在实际没有任何更改的情况下重新渲染大量组件可能会变得太慢。

¥Re-rendering components isn't bad - that's how React knows if it needs to update the DOM. But, re-rendering lots of components when nothing has actually changed can potentially get too slow if the list is too big.

我们可以尝试通过几种方法来解决这个问题。一种选择是 将所有 <TodoListItem> 组件封装在 React.memo(),这样它们只会在 props 实际发生变化时重新渲染。这通常是提高性能的不错选择,但它确实要求子组件始终接收相同的 props,直到某些内容真正发生变化。由于每个 <TodoListItem> 组件都接收一个待办事项作为属性,因此只有其中一个组件实际上应该获得更改后的属性并且必须重新渲染。

¥There's a couple ways we could try to fix this. One option is to wrap all the <TodoListItem> components in React.memo(), so that they only re-render when their props actually change. This is often a good choice for improving performance, but it does require that the child component always receives the same props until something really changes. Since each <TodoListItem> component is receiving a todo item as a prop, only one of them should actually get a changed prop and have to re-render.

另一种选择是让 <TodoList> 组件仅从存储中读取待办事项 ID 数组,并将这些 ID 作为 props 传递给子 <TodoListItem> 组件。然后,每个 <TodoListItem> 都可以使用该 ID 来查找它需要的正确的待办事项对象。

¥Another option is to have the <TodoList> component only read an array of todo IDs from the store, and pass those IDs as props to the child <TodoListItem> components. Then, each <TodoListItem> can use that ID to find the right todo object it needs.

让我们试一试。

¥Let's give that a shot.

src/features/todos/TodoList.js
import React from 'react'
import { useSelector } from 'react-redux'
import TodoListItem from './TodoListItem'

const selectTodoIds = state => state.todos.map(todo => todo.id)

const TodoList = () => {
const todoIds = useSelector(selectTodoIds)

const renderedListItems = todoIds.map(todoId => {
return <TodoListItem key={todoId} id={todoId} />
})

return <ul className="todo-list">{renderedListItems}</ul>
}

这次,我们只从 <TodoList> 中的存储中选择一个待办事项 ID 数组,并将每个 todoId 作为 id 属性传递给子 <TodoListItem>

¥This time, we only select an array of todo IDs from the store in <TodoList>, and we pass each todoId as an id prop to the child <TodoListItem>s.

然后,在 <TodoListItem> 中,我们可以使用该 ID 值来读取我们的待办事项。我们还可以更新 <TodoListItem> 以根据待办事项的 ID 调度 "toggled" 操作。

¥Then, in <TodoListItem>, we can use that ID value to read our todo item. We can also update <TodoListItem> to dispatch the "toggled" action based on the todo's ID.

src/features/todos/TodoListItem.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'

import { availableColors, capitalize } from '../filters/colors'

const selectTodoById = (state, todoId) => {
return state.todos.find(todo => todo.id === todoId)
}

// Destructure `props.id`, since we only need the ID value
const TodoListItem = ({ id }) => {
// Call our `selectTodoById` with the state _and_ the ID value
const todo = useSelector(state => selectTodoById(state, id))
const { text, completed, color } = todo

const dispatch = useDispatch()

const handleCompletedChanged = () => {
dispatch({ type: 'todos/todoToggled', payload: todo.id })
}

// omit other change handlers
// omit other list item rendering logic and contents

return (
<li>
<div className="view">{/* omit other rendering output */}</div>
</li>
)
}

export default TodoListItem

但这有一个问题。我们之前说过,在选择器中返回新的数组引用会导致组件每次都重新渲染,现在我们在 <TodoList> 中返回一个新的 ID 数组。在这种情况下,如果 we'重新切换待办事项,因为我们' 仍然显示相同的待办事项,则 ID 数组的内容应该相同 - 我们还没有添加或删除任何内容。但是,包含这些 ID 的数组是一个新引用,因此 <TodoList> 将在确实不需要时重新渲染。

¥There's a problem with this, though. We said earlier that returning new array references in selectors causes components to re-render every time, and right now we're returning a new IDs array in <TodoList>. In this case, the contents of the IDs array should be the same if we're toggling a todo, because we're still showing the same todo items - we haven't added or deleted any. But, the array containing those IDs is a new reference, so <TodoList> will re-render when it really doesn't need to.

一种可能的解决方案是更改 useSelector 比较其值的方式以查看它们是否已更改。useSelector 可以将比较函数作为其第二个参数。使用旧值和新值调用比较函数,如果认为它们相同,则返回 true。如果它们相同,useSelector 不会使组件重新渲染。

¥One possible solution to this is to change how useSelector compares its values to see if they've changed. useSelector can take a comparison function as its second argument. A comparison function is called with the old and new values, and returns true if they're considered the same. If they're the same, useSelector won't make the component re-render.

React-Redux 有一个 shallowEqual 比较函数,我们可以用它来检查数组中的项目是否仍然相同。让我们尝试一下:

¥React-Redux has a shallowEqual comparison function we can use to check if the items inside the array are still the same. Let's try that:

src/features/todos/TodoList.js
import React from 'react'
import { useSelector, shallowEqual } from 'react-redux'
import TodoListItem from './TodoListItem'

const selectTodoIds = state => state.todos.map(todo => todo.id)

const TodoList = () => {
const todoIds = useSelector(selectTodoIds, shallowEqual)

const renderedListItems = todoIds.map(todoId => {
return <TodoListItem key={todoId} id={todoId} />
})

return <ul className="todo-list">{renderedListItems}</ul>
}

现在,如果我们切换待办事项,ID 列表将被视为相同,并且 <TodoList> 不必重新渲染。<TodoListItem> 将获得更新的 todo 对象并重新渲染,但其余所有设备仍将拥有现有的 todo 对象,根本不必重新渲染。

¥Now, if we toggle a todo item, the list of IDs will be considered the same, and <TodoList> won't have to re-render. The one <TodoListItem> will get an updated todo object and re-render, but all the rest of them will still have the existing todo object and not have to re-render at all.

如前所述,你还可以使用一种名为 一个 "记忆选择器" 的特殊选择器函数来帮助改进组件渲染,我们将在另一节中介绍如何使用它们。

¥As mentioned earlier, you can also use a specialized kind of selector function called a "memoized selector" to help improve component rendering, and we'll look at how to use those in another section.

你学到了什么

¥What You've Learned

我们现在有了一个可以运行的待办事项应用!我们的应用创建一个存储,使用 <Provider> 将存储传递到 React UI 层,然后调用 useSelectoruseDispatch 与我们的 React 组件中的存储进行通信。

¥We now have a working todo app! Our app creates a store, passes the store to the React UI layer using <Provider>, and then calls useSelector and useDispatch to talk to the store in our React components.

信息

尝试自己实现其余缺失的 UI 功能!以下是你需要添加的内容的列表:

¥Try implementing the rest of the missing UI features on your own! Here's a list of the things you'll need to add:

  • <TodoListItem> 组件中,使用 useDispatch 钩子来调度更改颜色类别和删除待办事项的操作

    ¥In <TodoListItem> component, use the useDispatch hook to dispatch actions for changing the color category and deleting the todo

  • <Footer> 中,使用 useDispatch 钩子来调度将所有待办事项标记为已完成、清除已完成的待办事项以及更改过滤器值的操作。

    ¥In <Footer>, use the useDispatch hook to dispatch actions for marking all todos as completed, clearing completed todos, and changing the filter values.

我们将在 第 7 部分:标准 Redux 模式 中介绍过滤器的实现。

¥We'll cover implementing the filters in Part 7: Standard Redux Patterns.

让我们看看应用现在的样子,包括我们为了保持简短而跳过的组件和部分:

¥Let's see how the app looks now, including the components and sections we skipped to keep this shorter:

概括
  • Redux 存储可以与任何 UI 层一起使用

    ¥Redux stores can be used with any UI layer

    • UI 代码总是订阅 store,获取最新状态,并重绘自身

      ¥UI code always subscribes to the store, gets the latest state, and redraws itself

  • React-Redux 是 React 的官方 Redux UI 绑定库

    ¥React-Redux is the official Redux UI bindings library for React

    • React-Redux 作为单独的 react-redux 包安装

      ¥React-Redux is installed as a separate react-redux package

  • useSelector 钩子让 React 组件从存储中读取数据

    ¥The useSelector hook lets React components read data from the store

    • 选择器函数将整个存储 state 作为参数,并根据该状态返回一个值

      ¥Selector functions take the entire store state as an argument, and return a value based on that state

    • useSelector 调用其选择器函数并返回选择器的结果

      ¥useSelector calls its selector function and returns the result from the selector

    • useSelector 订阅存储,并在每次分派操作时重新运行选择器。

      ¥useSelector subscribes to the store, and re-runs the selector each time an action is dispatched.

    • 每当选择器结果发生变化时,useSelector 都会强制组件使用新数据重新渲染

      ¥Whenever the selector result changes, useSelector forces the component to re-render with the new data

  • useDispatch 钩子让 React 组件将操作分派到存储

    ¥The useDispatch hook lets React components dispatch actions to the store

    • useDispatch 返回实际的 store.dispatch 函数

      ¥useDispatch returns the actual store.dispatch function

    • 你可以根据需要在组件内调用 dispatch(action)

      ¥You can call dispatch(action) as needed inside your components

  • <Provider> 组件使存储可供其他 React 组件使用

    ¥The <Provider> component makes the store available to other React components

    • 在整个 <App> 周围渲染 <Provider store={store}>

      ¥Render <Provider store={store}> around your entire <App>

下一步是什么?

¥What's Next?

现在我们的 UI 已经可以工作了,是时候看看如何让我们的 Redux 应用与服务器通信了。在 第 6 部分:异步逻辑 中,我们将讨论超时和 AJAX 调用等异步逻辑如何融入 Redux 数据流。

¥Now that our UI is working, it's time to see how to make our Redux app talk to a server. In Part 6: Async Logic, we'll talk about how asynchronous logic like timeouts and AJAX calls fit into the Redux data flow.