如何解决昂贵的自定义挂钩?

How to work around expensive custom hooks?

据我们所知,规则是:

Only Call Hooks at the Top Level. Don’t call Hooks inside loops, conditions, or nested functions.

所以我的问题是如何使用和设计昂贵的自定义挂钩?

给出这个钩子:

const useExpensiveHook = () => {
    // some code that uses other built-in hooks...
    const value = computeExpensiveValue();
    // some more code....
    return value;
}

如果该规则不存在,我的客户端代码将是:

const myComponent = ({isSuperFeatureEnabled}) => {
   let value;
   if (isSuperFeatureEnabled){
      value = useExpensiveHook();
   }

   return <div>{value}</div>;
}

我想到的解决方案是让钩子知道它应该退出,就像这样,使用一个标志:

const useExpensiveHook = ({enabled}) => {
    // some code that uses other built-in hooks...
    let value;
      if(enabled) {
          value = computeExpensiveValue();
      }
      else {
          value = undefined;
      }
    // some more code....
    return value;
};

和客户端代码:

const myComponent = ({isSuperFeatureEnabled}) => {
   const value = useExpensiveHook({enabled : isSuperFeatureEnabled});
   return <div>{value}</div>;
}

将标志传递给昂贵的挂钩是处理条件挂钩的正确方法吗?还有哪些选择?

在原来的例子中是钩子初始值是昂贵的,而不是钩子本身,computeExpensiveValue可以有条件地调用:

const [value, setValue] = useState(enabled ? computeExpensiveValue() : undefined);

在当前列出的示例中,useExpensiveHook 不是一个钩子,而是一些函数;它不使用 React 钩子。

引用规则的目的是让内置的钩子无条件调用,因为钩子的状态是由它们被调用的顺序决定的:

if (flipCoin())
  var [foo] = useState('foo');

var [bar] = useState('bar');

如果 useState('foo') 在下一个组件渲染时没有被调用,useState('bar') 成为第一个 useState 挂钩被调用并被视为 foo 状态,而第二个 useState 丢失,这种不一致会在渲染器中触发错误。

如果保证钩子调用的顺序不变,使用条件是可以接受的,但这在实践中很少可行。即使存在像 if (process.env.NODE_ENV === 'development') 这样看似恒定的条件,它也可能在运行时的某些情况下发生变化,并导致难以调试的上述问题。

正确:

useEffect(() => {
  if (varyingCondition)
    computeExpensiveValue();
});

不正确:

if (varyingCondition)
  useEffect(() => {
    computeExpensiveValue();
  });

此规则仅适用于内置挂钩和直接或间接调用它们的函数(所谓的自定义挂钩)。只要 computeExpensiveValue 内部不使用内置钩子,就可以有条件地调用它,如 'correct' 示例所示。

如果组件需要根据 prop 标志有条件地应用第三方 hook,应通过将其限制为初始 prop 值来保证条件不会随时间变化:

const Component = ({ expensive, optionalValue }) => {
  const isExpensive = useMemo(() => expensive, []);
  if (isExpensive)
    optionalValue = useExpensiveHook();
  return ...
}

这样 <Component expensive={flipCoin()} /> 不会违反规则,只会滥用组件。

由于在使用<Component expensive/>时应该知道是否需要昂贵的钩子,更简洁的方法是将此功能组合在高阶组件中,并根据需要使用不同的组件:

const withExpensive = Comp => props => {
  const optionalValue = useExpensiveHook();
  return <Comp optionalValue={optionalValue} ...props />;
}

const Component = ({ optionalValue }) => {
  return ...
}

const ExpensiveComponent = withExpensive(Component);

useState 的参数仅使用一次,因此如果您最初将 enabled 作为 false 传递给它,它永远不会执行 computeExpensiveValue。因此,您还需要添加一个 useEffect 调用。你可以改为设计你的钩子

const useExpensiveHook = ({enabled}) => {
    const [value, setValue] = useState(enabled ? computeExpensiveValue : undefined);

    useEffect(()=> {
      if(enabled) {
          const value = computeExpensiveValue();
          setValue(value);
      }
    }, [enabled]);

    // some more code.... 
    return value;
};