如何对从Redux的useSelector中派生的变量进行记忆化?

7
如何对我的 `rawTranscript` 变量进行记忆化,以便它不会触发下面的 `useEffect` ,从而导致昂贵的 `transcriptParser` 函数被触发?虽然我尝试了很多不同的方法,但事实是我正在使用 redux-hook (`useAppSelector`) 来捕获存储在 store 中的数据,这意味着我无法为初始挂载使用一个空依赖项 useEffect (hooks 不能在 useEffect 中)。出于同样的原因,我似乎也无法用一层`useMemo` 包裹 `useAppSelector`。
你有什么想法可以记忆化 `rawTranscript` 变量,使其不会重新触发 `useEffect`?
当使用redux-hook 在`useMemo`、`useEffect`、`useCallback`内时出现错误: >React Hook “useAppSelector” cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.
组件
const TranscriptCardController = (): JSX.Element => {
  const dispatch = useAppDispatch();
  // how to memoize rawTranscript?
  const rawTranscript = useAppSelector(selectRawTranscript);
  const parsedTranscript = useAppSelector(selectParsedTranscript);

  useEffect(() => {
    const parsedResult = transcriptParser(rawTranscript, undefined, 0.9);
    dispatch(updateParsedTranscript(parsedResult));
  }, [dispatch, rawTranscript]);

  // ...
};

选择器

export const selectRawTranscript = createSelector(
  (state: RootState) => state.transcript.rawTranscript,
  (rawTranscript): RawTranscript => rawTranscript
);
2个回答

6
如果您的 selectRawTranscript 函数仅从 store 中选择一个值,例如 state => state.transcript.raw,那么就不存在问题。只有当 rawTranscript 的值发生变化时,effect 才会运行--这正是应该的。
如果您的 selectRawTranscript 函数每次返回一个新对象(例如涉及到数组映射等),那么就需要在 selector 本身或组件中解决此问题。
最好的解决方法是使用 createSelector 创建一个记忆化的 selector。例如:
import {createSelector} from '@reduxjs/toolkit';

export const selectRawTranscript = createSelector(
  (state: RootState) => state.data.someRawValue,
  (rawValue) => rawValue.map(entry => entry.data)
);

选择器的第二部分是“组合器”,只有在第一部分选择的值更改时才会重新运行。这样可以获得一致的对象引用。

相等比较

如果您想在组件中修复此问题,则需要在 useAppSelector 上包含第二个参数(我假设它只是 useSelector 的类型化版本)。

此第二个参数允许您指定自定义相等函数,以便您更好地控制选择的数据何时被视为“更改”。通常使用浅相等比较,所以这实际上已经包含在 react-redux 包中了。

import { shallowEqual } from 'react-redux';
import { useAppSelector } from ...

const TranscriptCardController = (): JSX.Element => {
  const rawTranscript = useAppSelector(selectRawTranscript, shallowEqual);
...

注意:我无法确定您是否真的遇到了rawTranscript中不良更改的问题,因为您没有包括您的选择器函数。您可能考虑过度思考,并且这可能是一个非问题。

你好,谢谢您的回复!我已更新原帖以包括我使用的选择器。如果还有什么需要请告诉我。 - kevin
我看到你发布了另一个问题,Mark解释为什么createSelector在这里不起作用。你肯定想太多了 :) 你的原始代码很好。 - Linda Paiste

0
创建一个独立的 useCallback,其中您的 dispatch 将在每次 store 更新时运行,但只有当执行 callback 方法时,useEffect 才会执行。
const TranscriptCardController = (): JSX.Element => {
  const dispatch = useAppDispatch();
  // how to memoize rawTranscript?
  const rawTranscript = useAppSelector(selectRawTranscript);
  const parsedTranscript = useAppSelector(selectParsedTranscript);
  
  const callback = useCallback(() => {
    const parsedResult = transcriptParser(rawTranscript, undefined, 0.9);
    dispatch(updateParsedTranscript(parsedResult));
  }, [rawTranscript])


  useEffect(() => {
    const unsubscribe = callback()

    return unsubscribe
  }, [callback]);

  // ...
};

谢谢您的回复,很有趣。然而,我觉得它有点混乱,因此难以维护和构建。话虽如此,它确实让我思考了我所面临的潜在问题,经过更多的思考,也许记忆化并不适用于这种情况。通过探索您的示例,如果我们取消回调(当我们卸载时)重新安装时,它将重新触发初始的useCallback,因为rawTranscript仍然被视为一个新值。 - kevin
1
你不必使用 unsubscribe,我只是在 async 回调中加入它是因为它可能会导致内存泄漏问题,但是只有在再次触发 useCallback 时,useEffect 才会重新触发。 - Dániel Boros

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接