在组件内部定义自定义挂钩是不是有任何问题?

Posted

技术标签:

【中文标题】在组件内部定义自定义挂钩是不是有任何问题?【英文标题】:Are there any issues with defining a custom hook inside of a component?在组件内部定义自定义挂钩是否有任何问题? 【发布时间】:2019-12-14 14:19:46 【问题描述】:

我有一个组件多次使用相同的 React 钩子,使用传递给钩子内部组件的道具。对于这个例子,我使用了 useCallback 钩子和 finish 属性,这只是另一个要调用的函数:

const Example1 = ( finish ) => 
  const runA = useCallback(
    () => 
      console.log("running A");
      finish();
    ,
    [finish]
  );
  const runB = useCallback(
    () => 
      console.log("running B");
      finish();
    ,
    [finish]
  );

  return (
    <Fragment>
      <button onClick=runA>A</button>
      <button onClick=runB>B</button>
    </Fragment>
  );
;

我想通过定义我自己的自定义钩子来稍微干燥一下。我的第一次尝试直接在组件内部定义了钩子:

const Example2 = ( finish ) => 
  const useCustomHook = action =>
    useCallback(
      () => 
        action();
        finish();
      ,
      [finish] // See note 1 below
    );

  const runA = useCustomHook(() => console.log("running A"));
  const runB = useCustomHook(() => console.log("running B"));

  return (
    <Fragment>
      <button onClick=runA>A</button>
      <button onClick=runB>B</button>
    </Fragment>
  );
;

1——这个依赖数组应该是[action, finish]。这是我犯的一个错误,因为在创建此示例时我没有启用 linting 规则。我理解正确指定这些的重要性。

Full example on Codepen

这似乎可行,但examples for creating custom hooks 都将钩子放在文件的顶层。钩子常见问题解答are hooks slow because of creating functions in render? 解决了创建函数的性能,但示例是传递钩子的函数。

在组件内创建一个钩子是否有任何功能问题需要注意?如果这通常是可以接受的,那么在钩子中捕获组件的属性是否有任何具体问题?


一些答案​​质疑为什么我不想将钩子放在组件之外。主要原因是我希望避免这种解决方案的重复和冗长,如以下快速演示所示:

const useTopLevelCustomHook = ( finish, action ) =>
  useCallback(
    () => 
      action();
      finish();
    ,
    [action, finish]
  );

const Example3 = ( finish ) => 
  const runA = useTopLevelCustomHook(
    action: () => console.log("running A"),
    finish
  );
  const runB = useTopLevelCustomHook(
    action: () => console.log("running B"),
    finish
  );

  return (
    <Fragment>
      <button onClick=runA>A</button>
      <button onClick=runB>B</button>
    </Fragment>
  );
;

一些答案​​侧重于在组件内创建闭包,这不是我的意图。在我最初的 TypeScript 应用程序中,我将 Redux 操作与传入的 prop 结合使用。动作创建者是相当静态的,因为它们是被导入的:

import React,  useCallback  from 'react';
import  useDispatch  from 'react-redux';

import * as actions from './actions';

interface BuildMenuProps 
  close: () => void;


const BuildMenu: React.SFC<BuildMenuProps> = props => 
  const dispatch = useDispatch();

  const useDispatchAndClose = (action: () => void) => useCallback(
    () => 
      dispatch(action());
      props.close();
    ,
    [action, props, dispatch]
  );

  const compile = useDispatchAndClose(actions.performCompile);
  const compileToAssembly = useDispatchAndClose(actions.performCompileToAssembly);
  const compileToLLVM = useDispatchAndClose(actions.performCompileToLLVM);
  const compileToMir = useDispatchAndClose(actions.performCompileToMir);
  const compileToWasm = useDispatchAndClose(actions.performCompileToNightlyWasm);
  const execute = useDispatchAndClose(actions.performExecute);
  const test = useDispatchAndClose(actions.performTest);

  // JSX that uses these callbacks

将此与渲染函数外部的钩子进行对比,需要传入props.close

const useDispatchAndClose = (action: () => void, close: () => void) => 
  const dispatch = useDispatch();

  return useCallback(
    () => 
      dispatch(action());
      close();
    ,
    [action, close, dispatch]
  );


const BuildMenu: React.SFC<BuildMenuProps> = props => 
  const compile = useDispatchAndClose(actions.performCompile, props.close);
  const compileToAssembly = useDispatchAndClose(actions.performCompileToAssembly, props.close);
  const compileToLLVM = useDispatchAndClose(actions.performCompileToLLVM, props.close);
  const compileToMir = useDispatchAndClose(actions.performCompileToMir, props.close);
  const compileToWasm = useDispatchAndClose(actions.performCompileToNightlyWasm, props.close);
  const execute = useDispatchAndClose(actions.performExecute, props.close);
  const test = useDispatchAndClose(actions.performTest, props.close);

  // JSX that uses these callbacks

【问题讨论】:

我猜[action, props, dispatch] 也不正确:道具本身每次都是一本新字典。所以它不会工作。应该是[action, close, dispatch] 【参考方案1】:

我认为您的方法没有问题。每次渲染时,所有钩子仍然以相同的顺序无条件地调用。 React 无法在运行时区分 Example1Example2 中的代码结构与 React API 交互的方式。

这似乎可行,但创建自定义挂钩的示例都将挂钩放在文件的顶层。

文档中的自定义挂钩示例展示了如何创建可在多个组件中重用的挂钩。为此,他们需要处于顶层。您有不同的用例。

至于这在技术上是否是“自定义钩子”? React Hooks ESLint 规则肯定认为它是一个自定义钩子,否则你会收到关于在“既不是 React 函数组件也不是自定义 React Hook 函数”的函数中调用钩子的警告(例如,如果你重命名 @987654327 @ 到usingCustomHook 你会收到警告)。 ESLint 规则将在您的自定义挂钩上强制执行 rules of hooks。 Here is a sandbox 我故意在 useCallback 调用中遗漏了一个依赖项,并且 ESLint 规则报告了缺少的依赖项('greetee')。

我唯一需要注意的是,在 documentation 中确实说:

不要在循环、条件或嵌套函数中调用 Hooks。

您的自定义钩子可以被视为嵌套函数。如果将来由于我没有看到的原因确定这种定义自定义挂钩的方式存在问题,那么您可能会有一些风险,即 ESLint 规则稍后会以抱怨这种方式的方式进行更改/增强。

在组件内部创建钩子时是否需要注意任何功能问题?

我看不出这种使用方式与在顶层定义挂钩并传递给它的附加参数的等效代码有什么不同。

如果总体上可以接受,那么在钩子中捕获组件的属性是否有任何具体问题?

自定义钩子将在每次渲染时重新定义,因此它不会以任何有意义的方式“捕获”组件的属性,这可能会改变可观察的行为,只要您只在它所在的同一组件中无条件地调用它被定义。如果您将自定义钩子作为属性传递给另一个组件然后调用它,我认为这可能是有问题的(这将是一种有问题的使用模式,可能导致 ESLint 规则被收紧以抱怨你在做什么)。

【讨论】:

感谢您成为第一个回答我直接问题的人,而不是仅仅专注于一些关于什么使某物成为钩子的细节。 仅供参考——我也为此pinged Dan Abramov 看看他是否有任何理由不鼓励这样做。不知道他会不会回应。他对 Twitter 上人们的问题反应相当迅速,但他也受到很多人的抨击。 谢谢!这是您正在玩的危险游戏,因为 Dan 有编写非常好的 SO 答案的历史,并且可能会窃取您的复选标记... ;-) 我同意——我宁愿得到信息而不是复选标记。【参考方案2】:

我不会说它是一个自定义钩子,你只是将钩子包装到一个回调中。虽然它看起来像一个钩子,甚至被称为钩子,但它不是一个钩子,因为自定义钩子的主要思想是从组件中提取可重复的逻辑以降低复杂性。

我猜你对钩子想太多了。 你的例子可以这样写:

 const Example2 = ( finish, depA, depB ) => 
  const executeAnAction = useCallback(
    action => 
      action();
      finish();
    ,
    [finish]
  );

  const runA = useCallback(
    () => executeAnAction(() => console.log("running A", depA)),
    [depA, executeAnAction]
  );
  const runB = useCallback(
    () => executeAnAction(() => console.log("running B", depB)),
    [depB, executeAnAction]
  );

  return (
    <Fragment>
      <button onClick=runA>A</button>
      <button onClick=runB>B</button>
    </Fragment>
  );
;

这有点简单。

第二个原因:在您的示例中,自定义钩子使保持钩子的依赖关系处于实际状态有点困难。事实上useCustomHook 应该有[finish, action] 依赖(因为动作可以有自己的依赖)像eslint-plugin-react-hooks 这样的插件会警告你。不幸的是,eslint-plugin-react-hooks 将无法评估 useCustomHookaction 的依赖关系,但它会在 2 个简单的 useCallback 的情况下正常工作,就像我的示例中一样

这里是an article that highlights importance of correct dependencies of hooks。

【讨论】:

它不是一个钩子——你有没有任何引用来支持一个函数必须在组件之外定义为一个钩? 自定义挂钩的主要思想是提取可重复的逻辑 [...] 以降低复杂性——因为这就是我这样做的原因。 我同意我的第二个示例中的依赖项是不正确的,但我觉得这也说明了为什么我想要这样的功能;使用自定义钩子,我可以在一个地方解决这些问题,而不是在多个地方。 您的示例自定义钩子使得在实际状态下保持钩子的依赖关系有点困难 — 我不明白它是如何变得更加困难;你能扩展一下吗? 我没有直接回答我的问题:在组件内创建挂钩是否存在任何功能问题?。我知道您在争论它不是钩子,但在这种情况下,假设我从未说过“钩子”这个词,而只是定义了一个小函数并将其命名为 doCustomThing " 我看不出它是如何让它变得更困难的;你能扩展一下吗?" 好的,你已经将action 添加到依赖项中,但是你每次重新创建一个新的action渲染,也就是说,你的缓存每次都会失效,根本不起作用。 为了让它工作,你必须在传递给useTopLevelCustomHook之前用useMemouseCallback记住action。使用useTopLevelCustomHook 会很尴尬。【参考方案3】:

它是 javascript,所以你可以为所欲为,但你应该问问自己这是否有意义。

在组件中保留函数的唯一原因是它需要访问组件私有的变量。如果它是通用的(他们称其为 纯函数)并将所有参数作为参数工作,那么放置它的位置并不重要,因为它的放置存在相关的复杂性在组件内部 - 你为什么还要考虑这样做?我建议将它放在组件之外。这很有意义。

为了让你更清楚地理解推理,这样做:

   const useCustomHook = action =>
    useCallback(
      () => 
        action();
        finish();
      ,
      [finish]
    );

...您仍在为每次更新创建useCustomHook。要让useCallback 完成它的工作,它应该是您打算创建的函数的包装器,如下所示:

    const useCustomHook = useCallback(action =>
      () => 
        action();
        finish();
      ,
      [finish]
    );

您传递给useCustomHook 的箭头函数将在每次更新时创建;这不是有效地否定了您必须在那里添加的所有恶作剧吗?

只需将这些函数放在之外,你甚至都不需要把它变成一个自定义的钩子。

【讨论】:

我没有直接回答我的问题:在组件内创建挂钩是否存在任何功能问题?。您似乎暗示因为它在组件内的放置存在相关的复杂性,但您没有说明它们是什么。【参考方案4】:

正如其他答案所述,这个问题实际上与钩子无关,而是在功能组件中创建函数是否可以。答案是肯定的,在功能组件中创建函数是一种完全可以接受的模式。您只需要注意,如果性能问题确实冒出来,您可能需要在事后进行一些优化。在 useCallback 中包装你的相当静态的函数已经做了一些优化,所以我不认为这段代码会成为问题。

【讨论】:

您是不是还说组件内部的钩子不是钩子?如果是这样,您能否引用资源来支持该声明? 我没有说,我回答了你关于在函数组件中编写函数(钩子是函数)的性能的问题。 是的,我认为可能是这种情况,这就是我想仔细检查的原因。 但是,我的问题不是关于性能的。在这个问题中,我包含了我所做的研究(链接到 React 常见问题解答中关于在渲染中创建函数的性能影响)并声明(强调添加):是否有任何 功能问题注意. 功能问题是什么意思?

以上是关于在组件内部定义自定义挂钩是不是有任何问题?的主要内容,如果未能解决你的问题,请参考以下文章

在自定义反应钩子中使用 axios

尽管状态发生了变化,但自定义钩子不会触发组件重新渲染

如何在自定义挂钩上使用 useCallback?

使用自定义挂钩时的重新渲染问题

如何仅在 React 自定义挂钩中获取数据一次?

如何将自定义挂钩添加到 Woocommerce 的自定义插件