跳到主要内容

常见问题解答

为什么当输入状态改变时,我的选择器没有重新计算?

请检查你的记忆化函数是否与你的状态更新函数(例如,如果你正在使用 Redux,那就是减速器)兼容。例如,使用 createSelector 创建的选择器将无法与一个状态更新函数一起工作,该函数会改变现有对象,而不是每次都创建一个新的对象。createSelector 使用身份检查(===)来检测输入是否已经改变,所以改变一个现有的对象不会触发选择器重新计算,因为改变一个对象并不会改变它的身份。请注意,如果你正在使用 Redux,改变状态对象几乎肯定是一个错误,详情请参考 这里

为什么当输入状态保持不变时,我的选择器正在重新计算?

要解决你的选择器中意外的重新计算问题,首先确保 inputStabilityCheck 被设置为 'always''once'。这个设置有助于通过监视输入的稳定性来进行调试。此外,利用 Output Selector Fields,如 recomputationsresetRecomputationsdependencyRecomputationsresetDependencyRecomputations。这些工具有助于识别问题的来源。

请关注 dependencyRecomputations 的计数。如果它在 recomputations 保持不变的情况下增加,这表明你的参数正在改变引用,但你的 input selectors 是稳定的,这通常是期望的行为。

要深入了解,你可以通过使用 argsMemoizeOptionsequalityCheck 来确定哪些参数的引用变化过于频繁。考虑以下示例:

FAQ/selectorRecomputing.ts
import { createSelector, lruMemoize } from 'reselect'

export interface RootState {
todos: { id: number; completed: boolean }[]
alerts: { id: number; read: boolean; type: string }[]
}

const selectAlertsByType = createSelector(
[
(state: RootState) => state.alerts,
(state: RootState, type: string) => type
],
(alerts, type) => alerts.filter(todo => todo.type === type),
{
argsMemoize: lruMemoize,
argsMemoizeOptions: {
// 这将检查传递给输出选择器的参数。
equalityCheck: (a, b) => {
if (a !== b) {
console.log('Changed argument:', a, 'to', b)
}
return a === b
}
}
}
)

我可以在没有Redux的情况下使用Reselect吗?

可以。Reselect没有依赖任何其他包,所以虽然它是设计用来和Redux一起使用的,但它也可以独立使用。只要数据是以不可变的方式更新的,它就可以用于任何普通的JS数据,比如典型的React状态值。

如何创建一个接受参数的选择器?

你提供的每一个input selectors都会被调用,并传入所有的选择器参数。你可以添加额外的输入选择器来提取参数,并将它们转发给result function,像这样:

const selectTodosByCategory = createSelector(
(state: RootState) => state.todos,
// 提取第二个参数以便传递
(state: RootState, category: string) => category,
(todos, category) => todos.filter(t => t.category === category)
)

在Reselect中创建一个接受参数的选择器时,重要的是要正确地构造你的输入和输出选择器。以下是需要考虑的关键点:

  1. 参数的一致性:确保所有位置参数在input selectors中的类型都是一致的。

  2. 选择性使用参数:设计每个选择器只使用其相关的参数,并忽略其余的。这是非常重要的,因为所有的input selectors都会接收到传递给output selector的相同参数。

假设我们有以下的状态结构:

interface RootState {
items: {
id: number
category: string
vendor: { id: number; name: string }
}[]
// ... 其他状态属性 ...
}

要创建一个基于category过滤items并排除特定id的选择器,你可以按照以下方式设置你的选择器:

const selectAvailableItems = createSelector(
[
// 第一个输入选择器从状态中提取items
(state: RootState) => state.items,
// 第二个输入选择器转发category参数
(state: RootState, category: string) => category,
// 第三个输入选择器转发ID参数
(state: RootState, category: string, id: number) => id
],
// 输出选择器使用提取的items,category和ID
(items, category, id) =>
items.filter(item => item.category === category && item.id !== id)
)

在内部,Reselect正在做这个:

// 输入选择器 #1
const items = (state: RootState, category: string, id: number) => state.items
// 输入选择器 #2
const category = (state: RootState, category: string, id: number) => category
// 输入选择器 #3
const id = (state: RootState, category: string, id: number) => id
// 输出选择器的结果
const finalResult =
// 结果函数
items.filter(item => item.category === category && item.id !== id)

在这个例子中,selectItemId期望它的第二个参数是一些简单的值,而selectVendorName期望第二个参数是一个对象。如果你调用selectItemById(state, 42)selectVendorName会出错,因为它试图访问42.name。Reselect的TS类型应该能检测到这个并阻止编译:

const selectItems = (state: RootState) => state.items

// 期望第二个参数是一个数字
const selectItemId = (state: RootState, itemId: number) => itemId

// 期望第二个参数是一个对象
const selectVendorName = (
state: RootState,
vendor: { id: number; name: string }
) => vendor.name

const selectItemById = createSelector(
[selectItems, selectItemId, selectVendorName],
(items, itemId, vendorName) => items[itemId]
)

可以自定义 memoization 行为吗?

可以。内置的 lruMemoize 记忆器对于许多用例都非常有效,但是它可以被自定义或替换为不同的记忆器。请参阅这些示例

如何测试选择器?

选择器是纯函数 - 对于给定的输入,选择器应始终产生相同的结果。因此,它们很容易进行单元测试:用一组输入调用选择器,并断言结果值与预期的形状匹配。

FAQ/howToTest.test.ts
import { createSelector } from 'reselect'
import { expect, test } from 'vitest'

interface RootState {
todos: { id: number; completed: boolean }[]
alerts: { id: number; read: boolean }[]
}

const state: RootState = {
todos: [
{ id: 0, completed: false },
{ id: 1, completed: true }
],
alerts: [
{ id: 0, read: false },
{ id: 1, read: true }
]
}

// 使用 `Vitest` 或 `Jest`
test('选择器单元测试', () => {
const selectTodoIds = createSelector(
[(state: RootState) => state.todos],
todos => todos.map(({ id }) => id)
)
const firstResult = selectTodoIds(state)
const secondResult = selectTodoIds(state)
// 引用相等应该通过。
expect(firstResult).toBe(secondResult)
// 深度相等也应该通过。
expect(firstResult).toStrictEqual(secondResult)
selectTodoIds(state)
selectTodoIds(state)
selectTodoIds(state)
// 结果函数不应重新计算。
expect(selectTodoIds.recomputations()).toBe(1)
// 输入选择器不应重新计算。
expect(selectTodoIds.dependencyRecomputations()).toBe(1)
})

// 使用 `Chai`
test('选择器单元测试', () => {
const selectTodoIds = createSelector(
[(state: RootState) => state.todos],
todos => todos.map(({ id }) => id)
)
const firstResult = selectTodoIds(state)
const secondResult = selectTodoIds(state)
// 引用相等应该通过。
expect(firstResult).to.equal(secondResult)
// 深度相等也应该通过。
expect(firstResult).to.deep.equal(secondResult)
selectTodoIds(state)
selectTodoIds(state)
selectTodoIds(state)
// 结果函数不应重新计算。
expect(selectTodoIds.recomputations()).to.equal(1)
// 输入选择器不应重新计算。
expect(selectTodoIds.dependencyRecomputations()).to.equal(1)
})

我可以在多个组件实例之间共享选择器吗?

可以,尽管如果它们传入不同的参数,你将需要处理这个问题,以便记忆化能够一致地工作:

  • 如果使用 lruMemoize,传递更大的 maxSize(从 4.1.0+ 开始)
  • 使用 weakMapMemoize(从 5.0.0+ 开始)

有 TypeScript 类型定义吗?

有!Reselect 现在是用 TypeScript 本身编写的,所以它们应该能够正常工作。

我看到一个 TypeScript 错误:Type instantiation is excessively deep and possibly infinite

从 5.0.0 开始,你应该能够嵌套多达 30 个选择器,但是如果你仍然遇到这个问题,你可以参考这个评论,讨论这个问题,因为它与嵌套选择器有关。

我如何创建一个柯里化的选择器?

接受参数的选择器通常在 React-Redux 的 useSelector 中使用闭包来传递额外的参数:

function TodosList({ category }) {
const filteredTodos = useSelector(state =>
selectTodosByCategory(state, category)
)
}

如果你更喜欢使用柯里化的形式,你可以使用这个配方创建一个柯里化的选择器:

FAQ/currySelector.ts
import { createSelector } from 'reselect'
import type { RootState } from './selectorRecomputing'

export const currySelector = <
State,
Result,
Params extends readonly any[],
AdditionalFields
>(
selector: ((state: State, ...args: Params) => Result) & AdditionalFields
) => {
const curriedSelector = (...args: Params) => {
return (state: State) => {
return selector(state, ...args)
}
}
return Object.assign(curriedSelector, selector)
}

const selectTodoByIdCurried = currySelector(
createSelector(
[(state: RootState) => state.todos, (state: RootState, id: number) => id],
(todos, id) => todos.find(todo => todo.id === id)
)
)

或者为了重用性,你可以这样做:

FAQ/createCurriedSelector.ts
import type { weakMapMemoize, SelectorArray, UnknownMemoizer } from 'reselect'
import { createSelector } from 'reselect'
import { currySelector } from './currySelector'

export const createCurriedSelector = <
InputSelectors extends SelectorArray,
Result,
OverrideMemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize,
OverrideArgsMemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize
>(
...args: Parameters<
typeof createSelector<
InputSelectors,
Result,
OverrideMemoizeFunction,
OverrideArgsMemoizeFunction
>
>
) => {
return currySelector(createSelector(...args))
}

这个:

const selectTodoById = createSelector(
[(state: RootState) => state.todos, (state: RootState, id: number) => id],
(todos, id) => todos.find(todo => todo.id === id)
)

selectTodoById(state, 0)

这与下面的代码是一样的:

selectTodoByIdCurried(0)(state)

以前你需要这样做:

const todoById = useSelector(state => selectTodoById(state, id))

现在你可以这样做:

const todoById = useSelector(selectTodoByIdCurried(id))

如果你正在使用 React-Redux,你还可以创建一个自定义的钩子工厂函数:

FAQ/createParametricSelectorHook.ts
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'

interface RootState {
todos: {
id: number
completed: boolean
title: string
description: string
}[]
alerts: { id: number; read: boolean }[]
}

const state: RootState = {
todos: [
{
id: 0,
completed: false,
title: 'Figure out if plants are really plotting world domination.',
description: 'They may be.'
},
{
id: 1,
completed: true,
title: 'Practice telekinesis for 15 minutes',
description: 'Just do it'
}
],
alerts: [
{ id: 0, read: false },
{ id: 1, read: true }
]
}

const selectTodoById = createSelector(
[(state: RootState) => state.todos, (state: RootState, id: number) => id],
(todos, id) => todos.find(todo => todo.id === id)
)

export const createParametricSelectorHook = <
Result,
Params extends readonly unknown[]
>(
selector: (state: RootState, ...params: Params) => Result
) => {
return (...args: Params) => {
return useSelector((state: RootState) => selector(state, ...args))
}
}

export const useSelectTodo = createParametricSelectorHook(selectTodoById)

然后在你的组件内部:

FAQ/MyComponent.tsx
import type { FC } from 'react'
import { useSelectTodo } from './createParametricSelectorHook'

interface Props {
id: number
}

const MyComponent: FC<Props> = ({ id }) => {
const todo = useSelectTodo(id)
return <div>{todo?.title}</div>
}

如何为我的根状态创建一个预先输入类型的createSelector版本?

在使用Redux时,通常所有输入选择器的第一个参数都是(state: RootState)。创建一个预先输入类型的createSelector可以减少这种重复。

您可以通过定义一个扩展原始createSelector函数的实用类型来创建一个自定义的类型化版本的createSelector。以下是一个例子:

import type {
OutputSelector,
Selector,
SelectorArray,
UnknownMemoizer
} from 'reselect'
import { createSelector } from 'reselect'

interface RootState {
todos: { id: number; completed: boolean }[]
alerts: { id: number; read: boolean }[]
}

export type TypedCreateSelector<
State,
MemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize,
ArgsMemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize
> = <
InputSelectors extends readonly Selector<State>[],
Result,
OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction,
OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction
>(
...createSelectorArgs: Parameters<
typeof createSelector<
InputSelectors,
Result,
OverrideMemoizeFunction,
OverrideArgsMemoizeFunction
>
>
) => ReturnType<
typeof createSelector<
InputSelectors,
Result,
OverrideMemoizeFunction,
OverrideArgsMemoizeFunction
>
>

export const createAppSelector: TypedCreateSelector<RootState> = createSelector
注意

当前这种方法只支持以单一数组形式提供的input selectors

如果我想在不使用记忆化的情况下使用createSelector怎么办?

可能在极少数情况下,你可能想要使用createSelector的组合语法,但不应用任何记忆化。在这种情况下,创建一个Identity Function并将其用作记忆器:

FAQ/identity.ts
import { createSelectorCreator } from 'reselect'

const identity = <Func extends (...args: any[]) => any>(func: Func) => func

const createNonMemoizedSelector = createSelectorCreator({
memoize: identity,
argsMemoize: identity
})