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

我正在看一些关于 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;

他说,通过这种方式,我们可以创建可重用的下拉组件。我想知道这与定义一个普通的旧Drampdown组件并将道具传递到其中有何不同。在这种情况下,我能想到的唯一区别是,现在我们能够在父组件(即)中获取状态和setState,并直接从那里读取/设置子组件的状态(即组件输出)。然而,这是否被认为是一种反模式,因为我们正在破坏单向数据流?SomeComponentuseDropdown


交互式爱情
浏览 127回答 3
3回答

慕莱坞森

虽然对于如何定义自定义钩子以及应该包含什么逻辑没有硬核限制,但它是一种反模式,可以编写返回JSX的钩子。您应该评估每种方法给您带来的好处,然后决定特定的代码段使用钩子返回JSX有一些缺点当您编写返回 JSX 组件的钩子时,您实际上是在功能组件中定义组件,因此每次重新渲染时,您都将创建该组件的新实例。这将导致组件被卸载并再次安装。这对性能不利,如果您在组件中有状态登录,也会有缺陷,因为状态将随着父级的每次重新渲染而重置通过在钩子中定义 JSX 组件,您可以根据需要取消延迟加载组件的选项。对组件的任何性能优化都需要您使用,而这些优化并不能为您提供自定义比较器功能(如 React.memo)的灵活性useMemo另一方面的好处是,您可以控制父级组件的状态。但是,您仍然可以通过使用受控组件方法实现相同的逻辑import React, { useState } from "react";const Dropdown = Reat.memo((props) => {&nbsp; const { label, value, updateState, options } = props;&nbsp; const id = `use-dropdown-${label.replace(" ", "").toLowerCase()}`;&nbsp; return (&nbsp; &nbsp; <label htmlFor={id}>&nbsp; &nbsp; &nbsp; {label}&nbsp; &nbsp; &nbsp; <select&nbsp; &nbsp; &nbsp; &nbsp; id={id}&nbsp; &nbsp; &nbsp; &nbsp; value={value}&nbsp; &nbsp; &nbsp; &nbsp; onChange={e => updateState(e.target.value)}&nbsp; &nbsp; &nbsp; &nbsp; onBlur={e => updateState(e.target.value)}&nbsp; &nbsp; &nbsp; &nbsp; disabled={!options.length}&nbsp; &nbsp; &nbsp; >&nbsp; &nbsp; &nbsp; &nbsp; <option />&nbsp; &nbsp; &nbsp; &nbsp; {options.map(item => (&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <option key={item} value={item}>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {item}&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </option>&nbsp; &nbsp; &nbsp; &nbsp; ))}&nbsp; &nbsp; &nbsp; </select>&nbsp; &nbsp; </label>&nbsp; );});export default Dropdown;并将其用作import React, { useState, useEffect } from "react";import useDropdown from "./useDropdown";const SomeComponent = () => {&nbsp; const [animal, updateAnimal] = useState("dog");&nbsp; const [breed, updateBreed] = useState("");&nbsp; return (&nbsp; &nbsp; <div className="search-params">&nbsp; &nbsp; &nbsp; <form>&nbsp; &nbsp; &nbsp; &nbsp; <label htmlFor="location">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Location&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <input&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; id="location"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value={location}&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; placeholder="Location"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; onChange={e => updateLocation(e.target.value)}&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; />&nbsp; &nbsp; &nbsp; &nbsp; </label>&nbsp; &nbsp; &nbsp; &nbsp; <Dropdown label="animal" value={animal} updateState={updateAnimal} options={ANIMALS}/>&nbsp; &nbsp; &nbsp; &nbsp; <Dropdown label="breed" value={breed} updateState={updateBreed} options={breeds}/>&nbsp; &nbsp; &nbsp; &nbsp; <button>Submit</button>&nbsp; &nbsp; &nbsp; </form>&nbsp; &nbsp; </div>&nbsp; );};export default SomeComponent;

潇湘沐

反模式是一个直言不讳的短语,用于描述其他开发人员不同意的简单或复杂的解决方案。我同意德鲁的观点,即钩子打破了传统的设计,做了比它应该做的更多的事情。根据&nbsp;React 的钩子文档,钩子的目的是允许你使用状态和其他 React 特性,而无需编写类。这通常被认为是设置状态,执行计算任务,在异步事务中执行API或其他查询,以及响应用户输入。理想情况下,功能组件应该可以与类组件互换,但实际上,这要困难得多。用于创建下拉列表组件的特定解决方案虽然有效,但并不是一个好的解决方案。为什么?这是令人困惑的,它不是不言自明的,很难理解发生了什么。使用钩子,它们应该很简单并执行单个任务,例如按钮回调处理程序,计算并返回记忆结果,或执行通常委派给的其他一些任务。this.doSomething()返回 JSX 的钩子根本不是真正的钩子,它们只是功能组件,即使它们对钩子使用正确的前缀命名约定。在 React 和组件更新的单向通信方面也存在混乱。对数据可以通过哪种方式没有限制,并且可以以与Angular类似的方式进行处理。有一些库允许您订阅和发布对共享类属性的更改,这些库将更新侦听的任何 UI 组件,并且该组件也可以更新它。您还可以使用 RxJS 随时进行异步更改,从而更新 UI。mobx具体示例确实避开了 SOLID 原则,为父组件提供输入点来控制子组件的数据。这是典型的强类型语言,如Java,其中进行异步通信更加困难(现在不是真正的问题,但曾经是)。父组件没有理由不能更新子组件 - 这是 React 的基本组成部分。添加的抽象越多,添加的复杂性就越高,故障点就越多。添加异步函数、可观察量 (mobx/rxjs) 或上下文的使用可以减少直接数据耦合,但它将创建更复杂的解决方案。

翻阅古今

我同意 Drew 的观点,即使用自定义钩子仅返回基于函数参数的 jsx 会破坏传统的组件抽象。为了扩展这一点,我可以想出四种不同的方法来使用 React 中的 jsx。静态 JSX如果jsx不依赖于状态/道具,你可以把它定义为一个甚至在你的组件之外。如果您有一系列内容,这尤其有用。const例:const myPs =&nbsp;[&nbsp;<p key="who">My name is...</p>,&nbsp;<p key="what">I am currently working as a...</p>,&nbsp;<p key="where">I moved to ...</p>,];const Component = () => (&nbsp; <>&nbsp; &nbsp;{ myPs.map(p => p) }&nbsp; </>);元件对于 jsx 的有状态和无状态部分。组件是将 UI 分解为可维护和可重用部分的 React 方式。上下文上下文提供程序返回 jsx(因为它们也是“只是”组件)。通常,您只需将子组件包装在要提供的上下文中,如下所示:&nbsp; return (&nbsp; &nbsp; <UserContext.Provider value={context}>&nbsp; &nbsp; &nbsp; {children}&nbsp; &nbsp; </UserContext.Provider>&nbsp; );但上下文也可用于开发全局组件。想象一个维护全局模式对话的对话上下文。目标是永远不要一次打开多个模式对话框。您可以使用上下文来管理对话框的状态,但也可以通过上下文提供程序组件呈现全局对话框 jsx:function DialogProvider({ children }) {&nbsp; const [showDialog, setShowDialog] = useState(false);&nbsp; const [dialog, setDialog] = useState(null);&nbsp; const openDialog = useCallback((newDialog) => {&nbsp; &nbsp; setDialog(newDialog);&nbsp; &nbsp; setShowDialog(true);&nbsp; }, []);&nbsp; const closeDialog = useCallback(() => {&nbsp; &nbsp; setShowDialog(false);&nbsp; &nbsp; setDialog(null);&nbsp; }, []);&nbsp; const context = {&nbsp; &nbsp; isOpen: showDialog,&nbsp; &nbsp; openDialog,&nbsp; &nbsp; closeDialog,&nbsp; };&nbsp; return (&nbsp; &nbsp; <DialogContext.Provider value={context}>&nbsp; &nbsp; &nbsp; { showDialog && <Dialog>{dialog}</Dialog> }&nbsp; &nbsp; &nbsp; {children}&nbsp; &nbsp; </DialogContext.Provider>&nbsp; );}更新上下文还将为用户更新 UI 中的全局对话框。设置新对话框将删除旧对话框。定制挂钩通常,钩子是封装要在组件之间共享的逻辑的好方法。我看到它们被用作复杂上下文的抽象层。想象一下,一个非常复杂,你的大多数组件只关心用户是否登录,你可以通过自定义钩子将其抽象出来。UserContextuseIsLoggedInconst useIsLoggedIn = () => {&nbsp; const { user } = useContext(UserContext);&nbsp; const [isLoggedIn, setIsLoggedIn] = useState(!!user);&nbsp; useEffect(() => {&nbsp; &nbsp; setIsLoggedIn(!!user);&nbsp; }, [user]);&nbsp; return isLoggedIn;};另一个很好的例子是一个钩子,它结合了你实际上想要在不同组件/容器中重用(而不是共享)的状态:const useStatus = () => {&nbsp; const [status, setStatus] = useState(LOADING_STATUS.IS_IDLE);&nbsp; const [isLoading, setIsLoading] = useState(false);&nbsp; useEffect(() => {&nbsp; &nbsp; setIsLoading(status === LOADING_STATUS.IS_LOADING);&nbsp; }, [status]);&nbsp; return { status, setStatus, isLoading };};此挂钩创建 API 调用相关状态,您可以在处理 API 调用的任何组件中重用该状态。我举了一个例子,我实际上使用自定义钩子来渲染jsx,而不是使用组件:const useGatsbyImage = (src, alt) => {&nbsp; const { data } = useContext(ImagesContext);&nbsp; const fluid = useMemo(() => (&nbsp; &nbsp; data.allFile.nodes.find(({ relativePath }) => src === relativePath).childImageSharp.fluid&nbsp; ), [data, src]);&nbsp; return (&nbsp; &nbsp; <Img&nbsp; &nbsp; &nbsp; fluid={fluid}&nbsp; &nbsp; &nbsp; alt={alt}&nbsp; &nbsp; />&nbsp; );};我可以为此创建一个组件吗?当然,但我也只是抽象出一个上下文,对我来说,这是一个使用钩子的模式。反应不是固执己见的。您可以定义自己的约定。再一次,我认为德鲁已经给了你一个很好的答案。我希望我的例子能帮助你更好地了解 React 为你提供的不同工具的用法。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

JavaScript