【问题标题】:confusion about this React custom hook usage关于这个 React 自定义钩子用法的困惑
【发布时间】:2020-09-22 23:00:24
【问题描述】:

我在看一些关于 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 组件并将道具传递给它有什么不同。在这种情况下我能想到的唯一区别是现在我们能够在父组件中获取状态和 setState(即SomeComponent)并读取/设置子组件的状态(即@987654325 输出的组件@) 直接从那里。然而,这是否被认为是一种反模式,因为我们正在打破单向数据流?

【问题讨论】:

  • 我会投票反对反模式。首先,它只能在功能组件中重用,其次,正如您所指出的,它打破了“常规”组件抽象,将 props 传递给子组件。
  • 反模式似乎是目前最受欢迎的流行词。它本身不是一个反模式,因为它不遵循任何类型的模式,它只是一个令人困惑的解决方案。这与提倡或至少提出的设计不同,这会导致糟糕/糟糕的解决方案。此外,函数式组件是解决问题的一种合法方式,并且源于良好的老式函数式编程,这通常比基于类的架构更易于阅读和处理。

标签: javascript reactjs react-hooks


【解决方案1】:

虽然对于如何定义自定义钩子以及应该包含什么逻辑没有硬核限制,但编写返回 JSX 的钩子是一种反模式

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

使用钩子返回 JSX 有一些缺点

  • 当您编写一个返回 JSX 组件的钩子时,您实际上是在功能组件中定义该组件,因此在每次重新渲染时,您都将创建该组件的一个新实例。这将导致组件被卸载并再次安装。如果您在组件中有状态登录,这对性能不利并且还会出现错误,因为每次重新渲染父级时都会重置状态
  • 通过在钩子中定义 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;

【讨论】:

    【解决方案2】:

    反模式是一个直言不讳的短语,用于描述其他开发人员不同意的简单或复杂解决方案。我同意 Drew 的观点,即挂钩打破了传统设计,做得比它应该做的更多。

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

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

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

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

    具体的例子确实避开SOLID principles,为父组件提供输入点来控制子组件的数据。这是典型的强类型语言,例如 Java,在这些语言中进行异步通信更加困难(这几天不是真正的问题,但它曾经是)。父组件没有理由不能更新子组件——它是 React 的基础部分。添加的抽象越多,添加的复杂性就越高,故障点就越多。

    添加使用异步函数、observables (mobx/rxjs) 或上下文可以减少直接的数据耦合,但会创建更复杂的解决方案。

    【讨论】:

      【解决方案3】:

      我同意 Drew 的观点,即使用自定义挂钩仅基于函数参数返回 jsx 打破了传统的组件抽象。为了扩展这一点,我可以想到四种在 React 中使用 jsx 的不同方法。

      静态 JSX

      如果 jsx 不依赖于 state/props,您可以将其定义为 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 的有状态和无状态部分。组件是 React 将您的 UI 分解为可维护和可重用部分的方法。

      上下文

      上下文提供者返回 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}
          />
        );
      };
      

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

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

      【讨论】:

      • 谢谢。这就像一篇带有示例的完整文章。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-09-09
      • 2021-10-20
      • 2021-04-17
      • 1970-01-01
      • 2021-10-27
      • 1970-01-01
      • 2014-02-05
      相关资源
      最近更新 更多