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

IT技术 reactjs react-hooks
2021-05-26 03:26:08

众所周知,规则是:

仅在顶层调用挂钩。不要在循环、条件或嵌套函数中调用 Hook。

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

鉴于这个钩子:

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>;
}

将标志传递给昂贵的钩子是处理条件钩子的正确方法吗?其他选项是什么?

2个回答

在原始示例中,钩子初始值是昂贵的,而不是钩子本身,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在内部不使用内置钩子,就可以有条件地调用它,如“正确”示例所示。

如果组件需要根据 prop 标志有条件地应用第三方钩子,应通过将其限制为初始 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);

to 的参数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;
};