关于这个React自定义钩子用法的困惑

时间:2020-06-04 03:40:31

标签: javascript reactjs react-hooks

我正在看一些有关React Hooks的教程,作者在该教程中创建了一个useDropdown钩子,用于呈现可重复使用的下拉菜单。代码是这样的

import React, { useState } from "react";

const useDropdown = (label, defaultState, options) => {
  const [state, updateState] = useState(defaultState);
  const id = `use-dropdown-${label.replace(" ", "").toLowerCase()}`;
  const Dropdown = () => (
    <label htmlFor={id}>
      {label}
      <select
        id={id}
        value={state}
        onChange={e => updateState(e.target.value)}
        onBlur={e => updateState(e.target.value)}
        disabled={!options.length}
      >
        <option />
        {options.map(item => (
          <option key={item} value={item}>
            {item}
          </option>
        ))}
      </select>
    </label>
  );
  return [state, Dropdown, updateState];
};

export default useDropdown;

他在类似这样的组件中使用了

import React, { useState, useEffect } from "react";
import useDropdown from "./useDropdown";

const SomeComponent = () => {
  const [animal, AnimalDropdown] = useDropdown("Animal", "dog", ANIMALS);
  const [breed, BreedDropdown, updateBreed] = useDropdown("Breed", "", breeds);

  return (
    <div className="search-params">
      <form>
        <label htmlFor="location">
          Location
          <input
            id="location"
            value={location}
            placeholder="Location"
            onChange={e => updateLocation(e.target.value)}
          />
        </label>
        <AnimalDropdown />
        <BreedDropdown />
        <button>Submit</button>
      </form>
    </div>
  );
};

export default SomeComponent;

他说,这样我们可以创建可重用的下拉组件。我想知道这与定义一个简单的旧Dropdown组件并将props传递给它有什么不同。在这种情况下,我能想到的唯一区别是,现在我们可以获取父组件中的state和setState(即SomeComponent)并读取/设置子组件的状态(即useDropdown)直接从那里。但是,由于我们正在打破单向数据流的方式,因此这被视为反模式吗?

3 个答案:

答案 0 :(得分:7)

尽管对如何定义自定义钩子以及应包含的逻辑没有硬性限制,但它是一种反模式,用于编写返回JSX的钩子

您应该评估每种方法给您带来的好处,然后确定特定的代码段

使用挂钩返回JSX有一些弊端

  • 编写返回JSX组件的挂钩时,实际上是在功能组件中定义组件,因此在每次重新渲染时,您都将创建该组件的新实例。这将导致组件被卸载并再次安装。如果您在组件内进行有状态登录,那么这对性能会造成不利影响,而且还会带来bug,因为每次重新渲染父项时,状态都会重置。
  • 通过在挂钩中定义JSX组件,您可以根据需要取消延迟加载组件的选项。
  • 对组件的任何性能优化都将要求您使用useMemo,这不能为您提供自定义比较器函数(如React.memo)的灵活性

另一方面,好处是您可以控制父级中组件的状态。但是,您仍然可以通过使用受控组件方法来实现相同的逻辑

import React, { useState } from "react";

const Dropdown = Reat.memo((props) => {
  const { label, value, updateState, options } = props;
  const id = `use-dropdown-${label.replace(" ", "").toLowerCase()}`;
  return (
    <label htmlFor={id}>
      {label}
      <select
        id={id}
        value={value}
        onChange={e => updateState(e.target.value)}
        onBlur={e => updateState(e.target.value)}
        disabled={!options.length}
      >
        <option />
        {options.map(item => (
          <option key={item} value={item}>
            {item}
          </option>
        ))}
      </select>
    </label>
  );
});

export default Dropdown;

并将其用作

import React, { useState, useEffect } from "react";
import useDropdown from "./useDropdown";

const SomeComponent = () => {
  const [animal, updateAnimal] = useState("dog");
  const [breed, updateBreed] = useState("");

  return (
    <div className="search-params">
      <form>
        <label htmlFor="location">
          Location
          <input
            id="location"
            value={location}
            placeholder="Location"
            onChange={e => updateLocation(e.target.value)}
          />
        </label>
        <Dropdown label="animal" value={animal} updateState={updateAnimal} options={ANIMALS}/>
        <Dropdown label="breed" value={breed} updateState={updateBreed} options={breeds}/>
        <button>Submit</button>
      </form>
    </div>
  );
};

export default SomeComponent;

答案 1 :(得分:4)

Anti-pattern是一个直截了当的用语,用来描述其他开发人员不同意的简单的复杂解决方案。我同意Drew的观点,即钩子做得比应做的事多,破坏了传统设计。

根据React's hook documentation,挂钩的目的是使您无需编写类即可使用状态和其他React功能。通常认为这是设置状态performing computational tasks,以异步方式进行API或其他查询,并响应用户输入。理想情况下,功能组件应该可以与类组件互换,但实际上,要实现这一点要困难得多。

用于创建Dropdown组件的特定解决方案虽然可行,但却不是一个好的解决方案。为什么?这很令人困惑,不是自我解释,很难理解正在发生的事情。使用钩子,它们应该很简单并执行单个任务,例如,按钮回调处理程序,计算并返回记录的结果,或者执行通常将其委派给this.doSomething()的其他任务。

返回JSX的钩子根本不是钩子,它们只是功能组件,即使它们对钩子使用正确的前缀命名约定也是如此。

React和用于组件更新的单向通信也存在混淆。对数据传递的方式没有任何限制,并且可以按照与Angular类似的方式进行处理。有诸如mobx之类的库,该库允许您订阅和发布对共享类属性的更改,这将更新任何侦听的UI组件,并且该组件也可以对其进行更新。您还可以随时使用RxJS进行异步更改,以更新UI。

该特定示例确实避开了SOLID principles,为父组件提供了输入点以控制子组件的数据。这是强类型语言(例如Java)的典型代表,在Java中,进行异步通信更加困难(如今,这确实不是一个问题,但以前是这样)。没有理由为什么父组件不能更新子组件-这是React的基本组成部分。添加的抽象越多,添加的复杂度就越高,故障点就越多。

增加使用异步函数,可观察变量(mobx / rxjs)或上下文可以减少直接数据耦合,但是它将创建一个更复杂的解决方案。

答案 2 :(得分:2)

我同意Drew的观点,即使用自定义钩子仅基于函数参数返回jsx会破坏常规的组件抽象。对此进行扩展,我可以想到在React中使用jsx的四种不同方法。

静态JSX

如果jsx不依赖状态/属性,则即使在组件外部也可以将其定义为const。如果您有一系列内容,这将特别有用。

示例:

const myPs = 
[
 <p key="who">My name is...</p>,
 <p key="what">I am currently working as a...</p>,
 <p key="where">I moved to ...</p>,
];

const Component = () => (
  <>
   { myPs.map(p => p) }
  </>
);

组件

适用于jsx的有状态和无状态部分。组件是将UI分解为可维护和可重用的部分的React方法。

上下文

上下文提供者返回jsx(因为它们也是“公正”的组件)。通常,您只需将子组件包装在要提供的上下文中,如下所示:

  return (
    <UserContext.Provider value={context}>
      {children}
    </UserContext.Provider>
  );

但是上下文也可以用于开发全局组件。想象一个对话框上下文,它维护一个全局模式对话框。目标是永远不要一次打开多个模式对话框。您可以使用上下文来管理对话框的状态,还可以通过上下文提供程序组件来呈现全局对话框jsx:

function DialogProvider({ children }) {
  const [showDialog, setShowDialog] = useState(false);
  const [dialog, setDialog] = useState(null);

  const openDialog = useCallback((newDialog) => {
    setDialog(newDialog);
    setShowDialog(true);
  }, []);

  const closeDialog = useCallback(() => {
    setShowDialog(false);
    setDialog(null);
  }, []);

  const context = {
    isOpen: showDialog,
    openDialog,
    closeDialog,
  };

  return (
    <DialogContext.Provider value={context}>
      { showDialog && <Dialog>{dialog}</Dialog> }
      {children}
    </DialogContext.Provider>
  );
}

更新上下文还将为用户更新UI内的全局对话框。设置一个新对话框将删除旧对话框。

自定义挂钩

通常,钩子是封装要在组件之间共享的逻辑的好方法。我已经看到它们被用作复杂上下文的抽象层。想象一下一个非常复杂的UserContext,并且您的大多数组件都在乎用户是否登录,您可以通过自定义的useIsLoggedIn钩子将其抽象化。

const useIsLoggedIn = () => {
  const { user } = useContext(UserContext);
  const [isLoggedIn, setIsLoggedIn] = useState(!!user);

  useEffect(() => {
    setIsLoggedIn(!!user);
  }, [user]);
  return isLoggedIn;
};

另一个很好的例子是一个挂钩,它结合了您实际上要在不同的组件/容器中重用(而不是共享)的状态:

const useStatus = () => {
  const [status, setStatus] = useState(LOADING_STATUS.IS_IDLE);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    setIsLoading(status === LOADING_STATUS.IS_LOADING);
  }, [status]);

  return { status, setStatus, isLoading };
};

此钩子创建与API调用相关的状态,您可以在处理API调用的任何组件中重用该状态。

我有一个示例,其中我实际上使用自定义钩子来渲染jsx而不是使用组件:

const useGatsbyImage = (src, alt) => {
  const { data } = useContext(ImagesContext);
  const fluid = useMemo(() => (
    data.allFile.nodes.find(({ relativePath }) => src === relativePath).childImageSharp.fluid
  ), [data, src]);

  return (
    <Img
      fluid={fluid}
      alt={alt}
    />
  );
};

我可以为此创建一个组件吗?当然可以,但是我也只是抽象出一个上下文,对我来说这是一个使用钩子的模式。反应是固执己见的。您可以定义自己的约定。

再次,我认为Drew已经给了您一个很好的答案。我希望我的示例可以帮助您更好地了解React为您提供的各种工具的使用。