关于这个 React 自定义 hook 的使用存在困惑。

27

我正在学习 React Hooks 的教程,其中作者创建了一个 useDropdown 钩子用于渲染可重用的下拉菜单。 代码如下:

```javascript // 在此处放置 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 传递给它有何不同。在这种情况下,我唯一能想到的区别是现在我们有能力在父组件(i.e. SomeComponent)中获取状态和 setState,并直接从那里读取/设置子组件(i.e. 由 useDropdown 输出的组件)的状态。但是,这是否被视为反模式,因为我们正在打破单向数据流?

7
我会进行反模式投票。首先,它只能在功能组件中重复使用;其次,正如你所指出的,它会破坏“传递属性给子组件”的“常规”组件抽象。 - Drew Reese
3
反模式似乎是目前最受欢迎的流行词。它本身并不是一种反模式,因为它没有遵循任何一种模式,只是一种令人困惑的解决方案。这与被提倡或至少呈现出来的设计导致了糟糕/不良的解决方案是不同的。此外,函数组件是解决问题的合法方式,并且具有良好的函数式编程根源,通常比基于类的架构更易于阅读和处理。 - Justin Mitchell
3个回答

19

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

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

使用钩子返回JSX存在一些缺点:

  • 当您编写返回JSX组件的钩子时,实质上是在函数组件内定义组件,因此在每次重新渲染时,您将创建组件的新实例。这将导致组件被卸载并重新挂载,这对性能来说是不好的,如果您在组件内使用有状态的逻辑,则会出现错误,因为每次父组件重新渲染时状态都会被重置
  • 通过在钩子中定义JSX组件,您取消了按需加载组件的选项。
  • 对组件的任何性能优化都需要使用useMemo ,它不像React.memo那样具有自定义比较函数的灵活性

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

import React, { useState } from "react";

const Dropdown = React.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;

8
“反模式”是一个非常生硬的短语,用来描述其他开发人员不认同的简单或复杂解决方案。我同意Drew的观点,即钩子打破了传统设计,超出了它应该做的事情。
根据React的钩子文档,钩子的目的是允许您使用状态和其他React功能而不编写类。这通常被认为是设置状态、执行计算任务、以异步方式执行API或其他查询,并响应用户输入。理想情况下,函数组件应该可以与类组件互换,但实际上,这要困难得多。
创建Dropdown组件的特定解决方案虽然可行,但并不是一个好的解决方案。为什么呢?它很令人困惑,不够自说明,很难理解正在发生的事情。使用钩子时,它们应该简单并执行单个任务,例如按钮回调处理程序、计算并返回备忘录结果,或执行通常委派给this.doSomething()的其他任务。
返回JSX的钩子实际上根本不是钩子,它们只是函数组件,即使它们使用了正确的前缀命名约定。
也有关于React和组件更新的单向通信的困惑。数据传递的方向没有限制,可以像Angular一样处理。有一些库,如mobx,它允许您订阅和发布对共享类属性的更改,这将更新任何侦听的UI组件,该组件也可以进行更新。您还可以使用RxJS在任何时间进行异步更改,从而可以更新UI。
具体示例偏离了SOLID principles,提供了父组件控制子组件数据的输入点。这在强类型语言(例如Java)中很常见,其中进行异步通信更加困难(现在不是问题了,但过去曾经是)。没有理由不让父组件能够更新子组件 - 这是React的一个基本部分。您添加的抽象层越多,就会增加复杂性和失败点。
添加使用异步函数、可观察对象(mobx/rxjs)或上下文可以减少直接数据耦合,但会创建更复杂的解决方案。

8

我同意Drew的观点,使用自定义hook只是根据函数参数返回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) }
  </>
);

组件

组件是 React 中将 UI 拆分为可维护和可重用部分的方式,适用于 JSX 的有状态和无状态部分。

上下文

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

更新上下文也会更新用户界面中的全局对话框。设置新对话框将删除旧对话框。

自定义钩子

一般来说,钩子是一种很好的方式来封装你想在组件之间共享的逻辑。我曾经看到它们被用作复杂上下文的抽象层。想象一下一个非常复杂的 UserContext,而大多数组件只关心用户是否已登录,你可以通过自定义的 useIsLoggedIn 钩子来抽象它。

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

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

另一个很好的例子是将你实际想要在不同组件/container中重复使用的state结合起来的钩子:
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 提供的不同工具的用法。


谢谢。这就像是一篇带有示例的完整文章。 - Rishad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接