使用选择器导出数据
为什么好的 Redux 架构会保持最小状态并导出额外数据
¥Why good Redux architecture keeps state minimal and derives additional data
使用选择器函数导出数据和封装查找的原理
¥Principles of using selector functions to derive data and encapsulate lookups
如何使用 Reselect 库编写记忆选择器以进行优化
¥How to use the Reselect library to write memoized selectors for optimization
使用重新选择的高级技术
¥Advanced techniques for using Reselect
用于创建选择器的其他工具和库
¥Additional tools and libraries for creating selectors
编写选择器的最佳实践
¥Best practices for writing selectors
导出数据
¥Deriving Data
我们特别建议 Redux 应用应该 保持 Redux 状态最小化,并尽可能从该状态派生附加值。
¥We specifically recommend that Redux apps should keep the Redux state minimal, and derive additional values from that state whenever possible.
这包括计算过滤列表或汇总值之类的事情。例如,待办事项应用会将待办事项对象的原始列表保留在状态中,但每当更新状态时都会在状态之外派生出已过滤的待办事项列表。类似地,也可以在存储外部计算检查所有待办事项是否已完成或剩余待办事项数量。
¥This includes things like calculating filtered lists or summing up values. As an example, a todo app would keep an original list of todo objects in state, but derive a filtered list of todos outside the state whenever the state is updated. Similarly, a check for whether all todos have been completed, or number of todos remaining, can be calculated outside the store as well.
这样做有几个好处:
¥This has several benefits:
实际状态更容易阅读
¥The actual state is easier to read
计算这些附加值并使它们与其余数据保持同步所需的逻辑更少
¥Less logic is needed to calculate those additional values and keep them in sync with the rest of the data
原始状态仍然作为参考并且没有被替换
¥The original state is still there as a reference and isn't being replaced
这对于 React 状态来说也是一个很好的原则!很多时候,用户尝试定义一个 useEffect
钩子来等待状态值更改,然后使用一些派生值(如 setAllCompleted(allCompleted)
)设置状态。相反,可以在渲染过程中导出该值并直接使用,而无需将该值保存到状态中:
¥This is also a good principle for React state as well! Many times users tried to define a useEffect
hook that waits for a state value to change, and then sets state with some derived value like setAllCompleted(allCompleted)
. Instead, that value can be derived during the rendering process and used directly, without having to save the value into state at all:
function TodoList() {
const [todos, setTodos] = useState([])
// Derive the data while rendering
const allTodosCompleted = todos.every(todo => todo.completed)
// render with this value
}
使用选择器计算派生数据
¥Calculating Derived Data with Selectors
在典型的 Redux 应用中,派生数据的逻辑通常编写为我们称为选择器的函数。
¥In a typical Redux application, the logic for deriving data is usually written as functions we call selectors.
选择器主要用于封装从状态中查找特定值的逻辑、实际导出值的逻辑以及通过避免不必要的重新计算来提高性能。
¥Selectors are primarily used to encapsulate logic for looking up specific values from state, logic for actually deriving values, and improving performance by avoiding unnecessary recalculations.
你不需要对所有状态查找使用选择器,但它们是标准模式并且广泛使用。
¥You are not required to use selectors for all state lookups, but they are a standard pattern and widely used.
基本选择器概念
¥Basic Selector Concepts
"选择器功能" 是任何接受 Redux 存储状态(或状态的一部分)作为参数并返回基于该状态的数据的函数。
¥A "selector function" is any function that accepts the Redux store state (or part of the state) as an argument, and returns data that is based on that state.
选择器不必使用特殊的库来编写,并且将它们编写为箭头函数或 function
关键字并不重要。例如,所有这些都是有效的选择器函数:
¥Selectors don't have to be written using a special library, and it doesn't matter whether you write them as arrow functions or the function
keyword. For example, all of these are valid selector functions:
// Arrow function, direct lookup
const selectEntities = state => state.entities
// Function declaration, mapping over an array to derive values
function selectItemIds(state) {
return state.items.map(item => item.id)
}
// Function declaration, encapsulating a deep lookup
function selectSomeSpecificField(state) {
return state.some.deeply.nested.field
}
// Arrow function, deriving values from an array
const selectItemsWhoseNamesStartWith = (items, namePrefix) =>
items.filter(item => item.name.startsWith(namePrefix))
选择器函数可以具有你想要的任何名称。然而,我们建议在选择器函数名称前添加单词 select
以及所选值的描述.典型的例子如 selectTodoById
、selectFilteredTodos
和 selectVisibleTodos
。
¥A selector function can have any name you want. However, we recommend prefixing selector function names with the word select
combined with a description of the value being selected. Typical examples of this would look like selectTodoById
, selectFilteredTodos
, and selectVisibleTodos
.
如果你使用过 React-Redux 的 useSelector
钩子,你可能已经熟悉选择器函数的基本概念 - 我们传递给 useSelector
的函数必须是选择器:
¥If you've used the useSelector
hook from React-Redux, you're probably already familiar with the basic idea of a selector function - the functions that we pass to useSelector
must be selectors:
function TodoList() {
// This anonymous arrow function is a selector!
const todos = useSelector(state => state.todos)
}
选择器函数通常定义在 Redux 应用的两个不同部分中:
¥Selector functions are typically defined in two different parts of a Redux application:
在切片文件中,与 reducer 逻辑一起
¥In slice files, alongside the reducer logic
在组件文件中,无论是在组件外部,还是在
useSelector
调用中内联¥In component files, either outside the component, or inline in
useSelector
calls
选择器函数可以用在任何可以访问整个 Redux 根状态值的地方。这包括 useSelector
钩子、connect
的 mapState
函数、中间件、thunk 和 sagas。例如,thunk 和中间件可以访问 getState
参数,因此你可以在那里调用选择器:
¥A selector function can be used anywhere you have access to the entire Redux root state value. This includes the useSelector
hook, the mapState
function for connect
, middleware, thunks, and sagas. For example, thunks and middleware have access to the getState
argument, so you can call a selector there:
function addTodosIfAllowed(todoText) {
return (dispatch, getState) => {
const state = getState()
const canAddTodos = selectCanAddTodos(state)
if (canAddTodos) {
dispatch(todoAdded(todoText))
}
}
}
通常不可能在 reducer 内部使用选择器,因为切片 reducer 只能访问其自己的 Redux 状态切片,并且大多数选择器希望将整个 Redux 根状态作为参数给出。
¥It's not typically possible to use selectors inside of reducers, because a slice reducer only has access to its own slice of the Redux state, and most selectors expect to be given the entire Redux root state as an argument.
用选择器封装状态形状
¥Encapsulating State Shape with Selectors
使用选择器函数的第一个原因是处理 Redux 状态形状时的封装和可重用性。
¥The first reason to use selector functions is for encapsulation and reusability when dealing with your Redux state shape.
假设你的 useSelector
钩子之一对 Redux 状态的一部分进行了非常具体的查找:
¥Let's say that one of your useSelector
hooks makes a very specific lookup into part of your Redux state:
const data = useSelector(state => state.some.deeply.nested.field)
这是合法的代码,并且可以正常运行。但是,从架构上来说,这可能不是最好的想法。想象一下,你有几个需要访问该字段的组件。如果你需要更改该状态所在的位置,会发生什么情况?你现在必须去更改引用该值的每个 useSelector
钩子。因此,与 我们建议使用操作创建者来封装创建操作的详细信息 一样,我们建议定义可重用选择器来封装给定状态片段所在位置的知识。然后,你可以在代码库中、应用需要检索特定数据的任何位置多次使用给定的选择器函数。
¥That is legal code, and will run fine. But, it might not be the best idea architecturally. Imagine that you've got several components that need to access that field. What happens if you need to make a change to where that piece of state lives? You would now have to go change every useSelector
hook that references that value. So, in the same way that we recommend using action creators to encapsulate details of creating actions, we recommend defining reusable selectors to encapsulate the knowledge of where a given piece of state lives. Then, you can use a given selector function many times in the codebase, anywhere that your app needs to retrieve that particular data.
理想情况下,只有你的 reducer 函数和选择器应该知道确切的状态结构,因此如果你更改某些状态所在的位置,你只需要更新这两个逻辑。
¥Ideally, only your reducer functions and selectors should know the exact state structure, so if you change where some state lives, you would only need to update those two pieces of logic.
因此,直接在切片文件内定义可重用选择器通常是一个好主意,而不是总是在组件内定义它们。
¥Because of this, it's often a good idea to define reusable selectors directly inside slice files, rather than always defining them inside of a component.
选择器的一个常见描述是它们就像 "查询你的状态"。你并不关心查询到底是如何得出你需要的数据的,只关心你请求数据并返回结果。
¥One common description of selectors is that they're like "queries into your state". You don't care about exactly how the query came up with the data you needed, just that you asked for the data and got back a result.
通过记忆优化选择器
¥Optimizing Selectors with Memoization
选择器函数通常需要执行相对 "expensive" 的计算,或者创建作为新对象和数组引用的派生值。这可能会影响应用性能,原因如下:
¥Selector functions often need to perform relatively "expensive" calculations, or create derived values that are new object and array references. This can be a concern for application performance, for several reasons:
与
useSelector
或mapState
一起使用的选择器将在每次分派操作后重新运行,无论 Redux 根状态的哪个部分实际更新。当输入状态部分未更改时重新运行昂贵的计算会浪费 CPU 时间,而且输入很可能在大多数情况下都不会更改。¥Selectors used with
useSelector
ormapState
will be re-run after every dispatched action, regardless of what section of the Redux root state was actually updated. Re-running expensive calculations when the input state sections didn't change is a waste of CPU time, and it's very likely that the inputs won't have changed most of the time anyway.useSelector
和mapState
依赖===
对返回值的引用相等性检查来确定组件是否需要重新渲染。如果选择器始终返回新引用,即使派生数据实际上与上次相同,它也会强制组件重新渲染。这对于像map()
和filter()
这样返回新数组引用的数组操作尤其常见。¥
useSelector
andmapState
rely on===
reference equality checks of the return values to determine if the component needs to re-render. If a selector always returns new references, it will force the component to re-render even if the derived data is effectively the same as last time. This is especially common with array operations likemap()
andfilter()
, which return new array references.
举个例子,这个组件写得很糟糕,因为它的 useSelector
调用总是返回一个新的数组引用。这意味着即使输入 state.todos
切片没有更改,组件也会在每次分派操作后重新渲染:
¥As an example, this component is written badly, because its useSelector
call always returns a new array reference. That means the component will re-render after every dispatched action, even if the input state.todos
slice hasn't changed:
function TodoList() {
// ❌ WARNING: this _always_ returns a new reference, so it will _always_ re-render!
const completedTodos = useSelector(state =>
state.todos.map(todo => todo.completed)
)
}
另一个示例是需要执行一些 "expensive" 工作来转换数据的组件:
¥Another example is a component that needs to do some "expensive" work to transform data:
function ExampleComplexComponent() {
const data = useSelector(state => {
const initialData = state.data
const filteredData = expensiveFiltering(initialData)
const sortedData = expensiveSorting(filteredData)
const transformedData = expensiveTransformation(sortedData)
return transformedData
})
}
同样,此 "expensive" 逻辑将在每次分派操作后重新运行。它不仅可能会创建新的引用,而且除非 state.data
实际发生变化,否则不需要完成这项工作。
¥Similarly, this "expensive" logic will re-run after every dispatched action. Not only will it probably create new references, but it's work that doesn't need to be done unless state.data
actually changes.
因此,我们需要一种编写优化选择器的方法,以避免在传入相同输入时重新计算结果。这就是记忆化的想法出现的地方。
¥Because of this, we need a way to write optimized selectors that can avoid recalculating results if the same inputs are passed in. This is where the idea of memoization comes in.
记忆化是缓存的一种形式。它涉及跟踪函数的输入,并存储输入和结果以供以后参考。如果使用与以前相同的输入调用函数,则该函数可以跳过实际工作,并返回上次接收这些输入值时生成的相同结果。这样可以通过仅在输入发生更改时才执行工作并在输入相同时始终返回相同的结果引用来优化性能。
¥Memoization is a form of caching. It involves tracking inputs to a function, and storing the inputs and the results for later reference. If a function is called with the same inputs as before, the function can skip doing the actual work, and return the same result it generated the last time it received those input values. This optimizes performance by only doing work if inputs have changed, and consistently returning the same result references if the inputs are the same.
接下来,我们将了解一些用于编写记忆选择器的选项。
¥Next, we'll look at some options for writing memoized selectors.
使用 Reselect 编写记忆选择器
¥Writing Memoized Selectors with Reselect
Redux 生态系统传统上使用名为 重新选择 的库来创建记忆选择器函数。还有其他类似的库,以及围绕 Reselect 的多个变体和封装器 - 我们稍后会讨论这些。
¥The Redux ecosystem has traditionally used a library called Reselect to create memoized selector functions. There also are other similar libraries, as well as multiple variations and wrappers around Reselect - we'll look at those later.
createSelector
概述
¥createSelector
Overview
Reselect 提供了一个名为 createSelector
的函数来生成记忆选择器。createSelector
接受一个或多个 "输入选择器" 函数,加上一个 "输出选择器" 函数,并返回一个新的选择器函数供你使用。
¥Reselect provides a function called createSelector
to generate memoized selectors. createSelector
accepts one or more "input selector" functions, plus an "output selector" function, and returns a new selector function for you to use.
createSelector
作为 我们的官方 Redux 工具包包 的一部分包含在内,并重新导出以便于使用。
¥createSelector
is included as part of our official Redux Toolkit package, and is re-exported for ease of use.
createSelector
可以接受多个输入选择器,这些选择器可以作为单独的参数或数组提供。所有输入选择器的结果作为单独的参数提供给输出选择器:
¥createSelector
can accept multiple input selectors, which can be provided as separate arguments or as an array. The results from all the input selectors are provided as separate arguments to the output selector:
const selectA = state => state.a
const selectB = state => state.b
const selectC = state => state.c
const selectABC = createSelector([selectA, selectB, selectC], (a, b, c) => {
// do something with a, b, and c, and return a result
return a + b + c
})
// Call the selector function and get a result
const abc = selectABC(state)
// could also be written as separate arguments, and works exactly the same
const selectABC2 = createSelector(selectA, selectB, selectC, (a, b, c) => {
// do something with a, b, and c, and return a result
return a + b + c
})
当你调用选择器时,Reselect 将使用你提供的所有参数运行输入选择器,并查看返回的值。如果任何结果与之前不同,它将重新运行输出选择器,并将这些结果作为参数传递。如果所有结果都与上次相同,它将跳过重新运行输出选择器,而只返回之前缓存的最终结果。
¥When you call the selector, Reselect will run your input selectors with all of the arguments you gave, and looks at the returned values. If any of the results are ===
different than before, it will re-run the output selector, and pass in those results as the arguments. If all of the results are the same as the last time, it will skip re-running the output selector, and just return the cached final result from before.
这意味着 "输入选择器" 通常应该只提取并返回值,而 "输出选择器" 应该执行转换工作。
¥This means that "input selectors" should usually just extract and return values, and the "output selector" should do the transformation work.
一个常见的错误是编写一个提取值或进行某些推导的 "输入选择器",以及一个仅返回其结果的 "输出选择器":
¥A somewhat common mistake is to write an "input selector" that extracts a value or does some derivation, and an "output selector" that just returns its result:
// ❌ BROKEN: this will not memoize correctly, and does nothing useful!
const brokenSelector = createSelector(
state => state.todos,
todos => todos
)
任何仅返回其输入的 "输出选择器" 都是不正确的!输出选择器应该始终具有转换逻辑。
¥Any "output selector" that just returns its inputs is incorrect! The output selector should always have the transformation logic.
同样,记忆选择器不应该使用 state => state
作为输入!这将迫使选择器始终重新计算。
¥Similarly, a memoized selector should never use state => state
as an input! That will force the selector to always recalculate.
在典型的 Reselect 用法中,你将顶层 "输入选择器" 编写为普通函数,并使用 createSelector
创建查找嵌套值的记忆选择器:
¥In typical Reselect usage, you write your top-level "input selectors" as plain functions, and use createSelector
to create memoized selectors that look up nested values:
const state = {
a: {
first: 5
},
b: 10
}
const selectA = state => state.a
const selectB = state => state.b
const selectA1 = createSelector([selectA], a => a.first)
const selectResult = createSelector([selectA1, selectB], (a1, b) => {
console.log('Output selector running')
return a1 + b
})
const result = selectResult(state)
// Log: "Output selector running"
console.log(result)
// 15
const secondResult = selectResult(state)
// No log output
console.log(secondResult)
// 15
请注意,我们第二次调用 selectResult
时,"输出选择器" 并未执行。由于 selectA1
和 selectB
的结果与第一次调用的结果相同,因此 selectResult
能够返回第一次调用的记忆结果。
¥Note that the second time we called selectResult
, the "output selector" didn't execute. Because the results of selectA1
and selectB
were the same as the first call, selectResult
was able to return the memoized result from the first call.
createSelector
行为
¥createSelector
Behavior
需要注意的是,默认情况下,createSelector
仅记住最新的一组参数。这意味着,如果你使用不同的输入重复调用选择器,它仍然会返回结果,但必须不断重新运行输出选择器才能生成结果:
¥It's important to note that by default, createSelector
only memoizes the most recent set of parameters. That means that if you call a selector repeatedly with different inputs, it will still return a result, but it will have to keep re-running the output selector to produce the result:
const a = someSelector(state, 1) // first call, not memoized
const b = someSelector(state, 1) // same inputs, memoized
const c = someSelector(state, 2) // different inputs, not memoized
const d = someSelector(state, 1) // different inputs from last time, not memoized
此外,你可以将多个参数传递给选择器。重新选择将使用这些确切的输入调用所有输入选择器:
¥Also, you can pass multiple arguments into a selector. Reselect will call all of the input selectors with those exact inputs:
const selectItems = state => state.items
const selectItemId = (state, itemId) => itemId
const selectItemById = createSelector(
[selectItems, selectItemId],
(items, itemId) => items[itemId]
)
const item = selectItemById(state, 42)
/*
Internally, Reselect does something like this:
const firstArg = selectItems(state, 42);
const secondArg = selectItemId(state, 42);
const result = outputSelector(firstArg, secondArg);
return result;
*/
因此,你提供的所有 "输入选择器" 应接受相同类型的参数,这一点很重要。否则,选择器将会损坏。
¥Because of this, it's important that all of the "input selectors" you provide should accept the same types of parameters. Otherwise, the selectors will break.
const selectItems = state => state.items
// expects a number as the second argument
const selectItemId = (state, itemId) => itemId
// expects an object as the second argument
const selectOtherField = (state, someObject) => someObject.someField
const selectItemById = createSelector(
[selectItems, selectItemId, selectOtherField],
(items, itemId, someField) => items[itemId]
)
在此示例中,selectItemId
期望其第二个参数是某个简单值,而 selectOtherField
期望第二个参数是一个对象。如果你调用 selectItemById(state, 42)
,selectOtherField
将中断,因为它正在尝试访问 42.someField
。
¥In this example, selectItemId
expects that its second argument will be some simple value, while selectOtherField
expects that the second argument is an object. If you call selectItemById(state, 42)
, selectOtherField
will break because it's trying to access 42.someField
.
重新选择使用模式和限制
¥Reselect Usage Patterns and Limitations
嵌套选择器
¥Nesting Selectors
可以采用 createSelector
生成的选择器,并将它们用作其他选择器的输入。在此示例中,selectCompletedTodos
选择器用作 selectCompletedTodoDescriptions
的输入:
¥It's possible to take selectors generated with createSelector
, and use them as inputs for other selectors as well. In this example, the selectCompletedTodos
selector is used as an input to selectCompletedTodoDescriptions
:
const selectTodos = state => state.todos
const selectCompletedTodos = createSelector([selectTodos], todos =>
todos.filter(todo => todo.completed)
)
const selectCompletedTodoDescriptions = createSelector(
[selectCompletedTodos],
completedTodos => completedTodos.map(todo => todo.text)
)
传递输入参数
¥Passing Input Parameters
可以使用任意数量的参数来调用重新选择生成的选择器函数:selectThings(a, b, c, d, e)
。然而,对于重新运行输出来说,重要的不是参数的数量,也不是参数本身是否已更改为新引用。相反,它是关于已定义的 "输入选择器" 以及它们的结果是否已更改。同样,"输出选择器" 的参数仅基于输入选择器返回的内容。
¥A Reselect-generated selector function can be called with as many arguments as you want: selectThings(a, b, c, d, e)
. However, what matters for re-running the output is not the number of arguments, or whether the arguments themselves have changed to be new references. Instead, it's about the "input selectors" that were defined, and whether their results have changed. Similarly, the arguments for the "output selector" are solely based on what the input selectors return.
这意味着,如果要将其他参数传递到输出选择器,则必须定义从原始选择器参数中提取这些值的输入选择器:
¥This means that if you want to pass additional parameters through to the output selector, you must define input selectors that extract those values from the original selector arguments:
const selectItemsByCategory = createSelector(
[
// Usual first input - extract value from `state`
state => state.items,
// Take the second arg, `category`, and forward to the output selector
(state, category) => category
],
// Output selector gets (`items, category)` as args
(items, category) => items.filter(item => item.category === category)
)
然后你可以像这样使用选择器:
¥You can then use the selector like this:
const electronicItems = selectItemsByCategory(state, "electronics");
为了保持一致性,你可能需要考虑将附加参数作为单个对象(例如 selectThings(state, otherArgs)
)传递给选择器,然后从 otherArgs
对象中提取值。
¥For consistency, you may want to consider passing additional parameters to a selector as a single object, such as selectThings(state, otherArgs)
, and then extracting values from the otherArgs
object.
选择器工厂
¥Selector Factories
createSelector
的默认缓存大小仅为 1,并且这是针对每个选择器的唯一实例的。当单个选择器函数需要在具有不同输入的多个位置重用时,这会产生问题。
¥createSelector
only has a default cache size of 1, and this is per each unique instance of a selector. This creates problems when a single selector function needs to get reused in multiple places with differing inputs.
一种选择是创建 "选择器工厂" - 一个运行 createSelector()
并在每次调用时生成一个新的唯一选择器实例的函数:
¥One option is to create a "selector factory" - a function that runs createSelector()
and generates a new unique selector instance every time it's called:
const makeSelectItemsByCategory = () => {
const selectItemsByCategory = createSelector(
[state => state.items, (state, category) => category],
(items, category) => items.filter(item => item.category === category)
)
return selectItemsByCategory
}
当多个相似的 UI 组件需要根据 props 派生不同的数据子集时,这特别有用。
¥This is particularly useful when multiple similar UI components need to derive different subsets of the data based on props.
替代选择器库
¥Alternative Selector Libraries
虽然 Reselect 是 Redux 中使用最广泛的选择器库,但还有许多其他库可以解决类似的问题,或扩展 Reselect 的功能。
¥While Reselect is the most widely used selector library with Redux, there are many other libraries that solve similar problems, or expand on Reselect's capabilities.
proxy-memoize
proxy-memoize
是一个相对较新的记忆选择器库,它使用独特的实现方法。它依赖 ES2015 Proxy
对象来跟踪尝试读取嵌套值,然后仅比较稍后调用的嵌套值以查看它们是否已更改。在某些情况下,这可以提供比重新选择更好的结果。
¥proxy-memoize
is a relatively new memoized selector library that uses a unique implementation approach. It relies on ES2015 Proxy
objects to track attempted reads of nested values, then compares only the nested values on later calls to see if they've changed. This can provide better results than Reselect in some cases.
一个很好的例子是派生待办事项描述数组的选择器:
¥A good example of this is a selector that derives an array of todo descriptions:
import { createSelector } from 'reselect'
const selectTodoDescriptionsReselect = createSelector(
[state => state.todos],
todos => todos.map(todo => todo.text)
)
不幸的是,如果 state.todos
内的任何其他值发生更改(例如切换 todo.completed
标志),这将重新计算派生数组。派生数组的内容是相同的,但由于输入 todos
数组发生了更改,因此必须计算新的输出数组,并且该数组具有新的引用。
¥Unfortunately, this will recalculate the derived array if any other value inside of state.todos
changes, such as toggling a todo.completed
flag. The contents of the derived array are identical, but because the input todos
array changed, it has to calculate a new output array, and that has a new reference.
与 proxy-memoize
相同的选择器可能如下所示:
¥The same selector with proxy-memoize
might look like:
import { memoize } from 'proxy-memoize'
const selectTodoDescriptionsProxy = memoize(state =>
state.todos.map(todo => todo.text)
)
与重新选择不同,proxy-memoize
可以检测到只有 todo.text
字段正在被访问,并且只有在 todo.text
字段之一发生更改时才会重新计算其余字段。
¥Unlike Reselect, proxy-memoize
can detect that only the todo.text
fields are being accessed, and will only recalculate the rest if one of the todo.text
fields changed.
它还具有内置的 size
选项,可让你为单个选择器实例设置所需的缓存大小。
¥It also has a built-in size
option, which lets you set the desired cache size for a single selector instance.
它与 Reselect 有一些权衡和差异:
¥It has some tradeoffs and differences from Reselect:
所有值都作为单个对象参数传入
¥All values are passed in as a single object argument
要求环境支持 ES2015
Proxy
对象(无 IE11)¥It requires that the environment supports ES2015
Proxy
objects (no IE11)它更神奇,而 Reselect 更明确
¥It's more magical, whereas Reselect is more explicit
基于
Proxy
的跟踪行为存在一些边缘情况¥There are some edge cases regarding the
Proxy
-based tracking behavior它较新,但使用较少
¥It's newer and less widely used
尽管如此,我们正式鼓励考虑使用 proxy-memoize
作为 Reselect 的可行替代方案。
¥All that said, we officially encourage considering using proxy-memoize
as a viable alternative to Reselect.
re-reselect
https://github.com/toomuchdesign/re-reselect 通过允许你定义 "按键选择器" 来改进 Reselect 的缓存行为。这用于在内部管理 Reselect 选择器的多个实例,这有助于简化多个组件的使用。
¥https://github.com/toomuchdesign/re-reselect improves Reselect's caching behavior, by allowing you to define a "key selector". This is used to manage multiple instances of Reselect selectors internally, which can help simplify usage across multiple components.
import { createCachedSelector } from 're-reselect'
const getUsersByLibrary = createCachedSelector(
// inputSelectors
getUsers,
getLibraryId,
// resultFunc
(users, libraryId) => expensiveComputation(users, libraryId)
)(
// re-reselect keySelector (receives selectors' arguments)
// Use "libraryName" as cacheKey
(_state_, libraryName) => libraryName
)
reselect-tools
有时可能很难追踪多个重新选择选择器如何相互关联,以及导致选择器重新计算的原因。https://github.com/skortchmark9/reselect-tools 提供了一种跟踪选择器依赖的方法,以及它自己的开发工具来帮助可视化这些关系并检查选择器值。
¥Sometimes it can be hard to trace how multiple Reselect selectors relate to each other, and what caused a selector to recalculate. https://github.com/skortchmark9/reselect-tools provides a way to trace selector dependencies, and its own DevTools to help visualize those relationships and check selector values.
redux-views
https://github.com/josepot/redux-views 与 re-reselect
类似,因为它提供了一种为每个项目选择唯一键以实现一致缓存的方法。它被设计为 Reselect 的近乎直接替代品,并且实际上被提议作为潜在 Reselect 版本 5 的一个选项。
¥https://github.com/josepot/redux-views is similar to re-reselect
, in that it provides a way to select unique keys for each item for consistent caching. It was designed as a near-drop-in replacement for Reselect, and actually proposed as an option for a potential Reselect version 5.
重新选择 v5 提案
¥Reselect v5 Proposal
我们在 Reselect 存储库中开启了路由图讨论,以找出 Reselect 未来版本的潜在增强功能,例如改进 API 以更好地支持更大的缓存大小、用 TypeScript 重写代码库以及其他可能的改进。我们欢迎在该讨论中提供更多社区反馈:
¥We've opened up a roadmap discussion in the Reselect repo to figure out potential enhancements to a future version of Reselect, such as improving the API to better support larger cache sizes, rewriting the codebase in TypeScript, and other possible improvements. We'd welcome additional community feedback in that discussion:
¥Reselect v5 Roadmap Discussion: Goals and API Design
将选择器与 React-Redux 一起使用
¥Using Selectors with React-Redux
使用参数调用选择器
¥Calling Selectors with Parameters
想要将附加参数传递给选择器函数是很常见的。但是,useSelector
始终使用一个参数调用提供的选择器函数 - Redux 根 state
。
¥It's common to want to pass additional arguments to a selector function. However, useSelector
always calls the provided selector function with one argument - the Redux root state
.
最简单的解决方案是将匿名选择器传递给 useSelector
,然后立即使用 state
和任何其他参数调用真正的选择器:
¥The simplest solution is to pass an anonymous selector to useSelector
, and then immediately call the real selector with both state
and any additional arguments:
import { selectTodoById } from './todosSlice'
function TodoListitem({ todoId }) {
// Captures `todoId` from scope, gets `state` as an arg, and forwards both
// to the actual selector function to extract the result
const todo = useSelector(state => selectTodoById(state, todoId))
}
创建唯一的选择器实例
¥Creating Unique Selector Instances
在很多情况下,选择器函数需要在多个组件之间重用。如果组件都使用不同的参数调用选择器,则会破坏记忆 - 选择器永远不会连续多次看到相同的参数,因此永远无法返回缓存的值。
¥There are many cases where a selector function needs to be reused across multiple components. If the components will all be calling the selector with different arguments, it will break memoization - the selector never sees the same arguments multiple times in a row, and thus can never return a cached value.
这里的标准方法是在组件中创建记忆选择器的唯一实例,然后将其与 useSelector
一起使用。这允许每个组件一致地将相同的参数传递给它自己的选择器实例,并且该选择器可以正确地记住结果。
¥The standard approach here is to create a unique instance of a memoized selector in the component, and then use that with useSelector
. That allows each component to consistently pass the same arguments to its own selector instance, and that selector can correctly memoize the results.
对于功能组件,通常使用 useMemo
或 useCallback
来完成:
¥For function components, this is normally done with useMemo
or useCallback
:
import { makeSelectItemsByCategory } from './categoriesSlice'
function CategoryList({ category }) {
// Create a new memoized selector, for each component instance, on mount
const selectItemsByCategory = useMemo(makeSelectItemsByCategory, [])
const itemsByCategory = useSelector(state =>
selectItemsByCategory(state, category)
)
}
对于具有 connect
的类组件,可以使用 mapState
的高级 "工厂功能" 语法来完成。如果 mapState
函数在第一次调用时返回一个新函数,则该函数将用作真正的 mapState
函数。这提供了一个闭包,你可以在其中创建新的选择器实例:
¥For class components with connect
, this can be done with an advanced "factory function" syntax for mapState
. If the mapState
function returns a new function on its first call, that will be used as the real mapState
function. This provides a closure where you can create a new selector instance:
import { makeSelectItemsByCategory } from './categoriesSlice'
const makeMapState = (state, ownProps) => {
// Closure - create a new unique selector instance here,
// and this will run once for every component instance
const selectItemsByCategory = makeSelectItemsByCategory()
const realMapState = (state, ownProps) => {
return {
itemsByCategory: selectItemsByCategory(state, ownProps.category)
}
}
// Returning a function here will tell `connect` to use it as
// `mapState` instead of the original one given to `connect`
return realMapState
}
export default connect(makeMapState)(CategoryList)
有效使用选择器
¥Using Selectors Effectively
虽然选择器是 Redux 应用中的常见模式,但它们经常被误用或误解。以下是正确使用选择器函数的一些准则。
¥While selectors are a common pattern in Redux applications, they are often misused or misunderstood. Here are some guidelines for using selector functions correctly.
定义选择器和 reducer
¥Define Selectors Alongside Reducers
选择器函数通常定义在 UI 层中,直接在 useSelector
调用内部。然而,这意味着不同文件中定义的选择器之间可以存在重复,并且函数是匿名的。
¥Selector functions are often defined in the UI layer, directly inside of useSelector
calls. However, this means that there can be repetition between selectors defined in different files, and the functions are anonymous.
与任何其他函数一样,你可以在组件外部提取一个匿名函数来为其命名:
¥Like any other function, you can extract an anonymous function outside the component to give it a name:
const selectTodos = state => state.todos
function TodoList() {
const todos = useSelector(selectTodos)
}
然而,应用的多个部分可能想要使用相同的查找。另外,从概念上讲,我们可能希望保留 todos
状态如何组织为 todosSlice
文件内的实现细节的知识,以便将其全部集中在一个位置。
¥However, multiple parts of the application may want to use the same lookups. Also, conceptually, we may want to keep the knowledge of how the todos
state is organized as an implementation detail inside the todosSlice
file, so that it's all in one place.
因此,最好定义可重用的选择器及其相应的缩减器。在这种情况下,我们可以从 todosSlice
文件中导出 selectTodos
:
¥Because of this, it's a good idea to define reusable selectors alongside their corresponding reducers. In this case, we could export selectTodos
from the todosSlice
file:
import { createSlice } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
todoAdded(state, action) {
state.push(action.payload)
}
}
})
export const { todoAdded } = todosSlice.actions
export default todosSlice.reducer
// Export a reusable selector here
export const selectTodos = state => state.todos
这样,如果我们碰巧对 todos 切片状态的结构进行更新,相关的选择器就在这里并且可以同时更新,而对应用的任何其他部分的更改最少。
¥That way, if we happen to make an update to the structure of the todos slice state, the relevant selectors are right here and can be updated at the same time, with minimal changes to any other parts of the app.
平衡选择器的用途
¥Balance Selector Usage
可能会向应用添加太多选择器。为每个字段添加单独的选择器函数并不是一个好主意!最终将 Redux 变成了类似于 Java 类的东西,每个字段都有 getter/setter 函数。它不会改进代码,而且可能会使代码变得更糟 - 维护所有这些额外的选择器需要花费大量额外的精力,并且很难跟踪在何处使用了哪些值。
¥It's possible to add too many selectors to an application. Adding a separate selector function for every single field is not a good idea! That ends up turning Redux into something resembling a Java class with getter/setter functions for every field. It's not going to improve the code, and it's probably going to make the code worse - maintaining all those extra selectors is a lot of additional effort, and it will be harder to trace what values are being used where.
同样,不要记住每个选择器!。仅当你真正导出结果并且导出的结果可能每次都会创建新的引用时才需要记忆。直接查找并返回值的选择器函数应该是普通函数,而不是记忆函数。
¥Similarly, don't make every single selector memoized!. Memoization is only needed if you are truly deriving results, and if the derived results would likely create new references every time. A selector function that does a direct lookup and return of a value should be a plain function, not memoized.
何时和何时不应该记忆的一些例子:
¥Some examples of when and when not to memoize:
// ❌ DO NOT memoize: will always return a consistent reference
const selectTodos = state => state.todos
const selectNestedValue = state => state.some.deeply.nested.field
const selectTodoById = (state, todoId) => state.todos[todoId]
// ❌ DO NOT memoize: deriving data, but will return a consistent result
const selectItemsTotal = state => {
return state.items.reduce((result, item) => {
return result + item.total
}, 0)
}
const selectAllCompleted = state => state.todos.every(todo => todo.completed)
// ✅ SHOULD memoize: returns new references when called
const selectTodoDescriptions = state => state.todos.map(todo => todo.text)
根据组件的需要重塑状态
¥Reshape State as Needed for Components
选择器不必将自己限制为直接查找 - 他们可以在内部执行任何需要的转换逻辑。这对于帮助准备特定组件所需的数据特别有价值。
¥Selectors do not have to limit themselves to direct lookups - they can perform any needed transformation logic inside. This is especially valuable to help prepare data that is needed by specific components.
Redux 状态通常具有 "raw" 形式的数据,因为 状态应该保持最小化,并且许多组件可能需要以不同的方式渲染相同的数据。你不仅可以使用选择器来提取状态,还可以根据特定组件的需要对其进行重塑。这可能包括从根状态的多个切片中提取数据、提取特定值、将不同的数据片段合并在一起,或任何其他有用的转换。
¥A Redux state often has data in a "raw" form, because the state should be kept minimal, and many components may need to present the same data differently. You can use selectors to not only extract state, but to reshape it as needed for this specific component's needs. That could include pulling data from multiple slices of the root state, extracting specific values, merging different pieces of the data together, or any other transformations that are helpful.
如果组件也有一些这样的逻辑,那很好,但是将所有这些转换逻辑拉出到单独的选择器中以获得更好的重用和可测试性可能是有益的。
¥It's fine if a component has some of this logic too, but it can be beneficial to pull all of this transformation logic out into separate selectors for better reuse and testability.
如果需要,全局化选择器
¥Globalize Selectors if Needed
编写切片缩减器和选择器之间存在固有的不平衡。切片 reducer 只知道其状态的一部分 - 对于 reducer 来说,它的 state
就是存在的全部,比如 todoSlice
中的 todos 数组。另一方面,选择器通常被编写为将整个 Redux 根状态作为它们的参数。这意味着他们必须知道该切片的数据在根状态中保存在何处,例如 state.todos
,即使在创建根 reducer 之前尚未真正定义(通常在应用范围的存储设置逻辑中)。
¥There's an inherent imbalance between writing slice reducers and selectors. Slice reducers only know about their one portion of the state - to the reducer, its state
is all that exists, such as the array of todos in a todoSlice
. Selectors, on the other hand, usually are written to take the entire Redux root state as their argument. This means that they have to know where in the root state this slice's data is kept, such as state.todos
, even though that's not really defined until the root reducer is created (typically in the app-wide store setup logic).
典型的切片文件通常同时具有这两种模式。这很好,尤其是在中小型应用中。但是,根据你的应用的架构,你可能需要进一步抽象选择器,以便它们不知道切片状态保存在哪里 - 它必须交给他们。
¥A typical slice file often has both of these patterns side-by-side. That's fine, especially in small or midsize apps. But, depending on your app's architecture, you may want to further abstract the selectors so that they don't know where the slice state is kept - it has to be handed to them.
我们将此模式称为 "globalizing" 选择器。"globalized" 选择器接受 Redux 根状态作为参数,并且知道如何找到相关的状态切片来执行真正的逻辑。"localized" 选择器只需要状态的一部分作为参数,而不知道或关心它在根状态中的位置:
¥We refer to this pattern as "globalizing" selectors. A "globalized" selector is one that accepts the Redux root state as an argument, and knows how to find the relevant slice of state to perform the real logic. A "localized" selector is one that expects just a piece of the state as an argument, without knowing or caring where that is in the root state:
// "Globalized" - accepts root state, knows to find data at `state.todos`
const selectAllTodosCompletedGlobalized = state =>
state.todos.every(todo => todo.completed)
// "Localized" - only accepts `todos` as argument, doesn't know where that came from
const selectAllTodosCompletedLocalized = todos =>
todos.every(todo => todo.completed)
通过将 "本地化" 选择器封装在一个知道如何检索正确的状态切片并将其向前传递的函数中,可以将它们转换为 "globalized" 选择器。
¥"Localized" selectors can be turned into "globalized" selectors by wrapping them in a function that knows how to retrieve the right slice of state and pass it onwards.
Redux Toolkit 的 createEntityAdapter
API 就是这种模式的一个例子。如果你不带参数调用 todosAdapter.getSelectors()
,它将返回一组 "localized" 选择器,这些选择器期望实体切片状态作为其参数。如果你调用 todosAdapter.getSelectors(state => state.todos)
,它会返回一组 "globalized" 选择器,这些选择器期望以 Redux 根状态作为参数进行调用。
¥Redux Toolkit's createEntityAdapter
API is an example of this pattern. If you call todosAdapter.getSelectors()
, with no argument, it returns a set of "localized" selectors that expect the entity slice state as their argument. If you call todosAdapter.getSelectors(state => state.todos)
, it returns a set of "globalized" selectors that expect to be called with the Redux root state as their argument.
拥有 "localized" 版本的选择器可能还有其他好处。例如,假设我们有一个高级场景,将 createEntityAdapter
数据的多个副本保留在存储中,例如跟踪房间的 chatRoomsAdapter
,然后每个房间定义都有一个 chatMessagesAdapter
状态来存储消息。我们无法直接查找每个房间的消息 - 我们首先必须检索房间对象,然后从中选择消息。如果我们有一组消息的 "localized" 选择器,这会更容易。
¥There may also be other benefits to having "localized" versions of selectors as well. For example, say we have an advanced scenario of keeping multiple copies of createEntityAdapter
data nested in the store, such as a chatRoomsAdapter
that tracks rooms, and each room definition then has a chatMessagesAdapter
state to store the messages. We can't directly look up the messages for each room - we first have to retrieve the room object, then select the messages out of that. This is easier if we have a set of "localized" selectors for the messages.
更多信息
¥Further Information
选择器库:
¥Selector libraries:
重新选择:https://github.com/reduxjs/reselect
¥Reselect: https://github.com/reduxjs/reselect
proxy-memoize
:https://github.com/dai-shi/proxy-memoizere-reselect
:https://github.com/toomuchdesign/re-reselectreselect-tools
:https://github.com/skortchmark9/reselect-toolsredux-views
:https://github.com/josepot/redux-views
Randy Coulman 有一系列精彩的博客文章,介绍选择器架构和全局化 Redux 选择器的不同方法,并进行权衡:
¥Randy Coulman has an excellent series of blog posts on selector architecture and different approaches for globalizing Redux selectors, with tradeoffs: