我看了一些关于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传递到其中有什么不同。在这种情况下,我能想到的唯一区别是,现在我们可以在父组件(即SomeComponent
)中获取state和setState,并直接从那里读取/设置子组件(即由useDropdown
输出的组件)的状态。然而,这是否被认为是一种反模式,因为我们正在打破单向数据流?
虽然对如何定义自定义钩子以及应该包含什么逻辑没有核心限制,但编写返回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;
我同意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>
);
}
更新上下文还将更新用户界面中的全局对话框。设置新对话框将删除旧对话框。
自定义挂钩
一般来说,钩子是封装希望在组件之间共享的逻辑的好方法。我看到它们被用作复杂上下文的抽象层。想象一个非常复杂的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并不固执己见。您可以定义自己的约定。
再说一遍,我认为德鲁已经给了你一个很好的答案。我希望我的例子能帮助你更好地了解React为你提供的不同工具的用法。
反模式是一个非常直率的短语,用来描述其他开发人员不同意的简单或复杂的解决方案。我同意德鲁的观点,即钩子打破了传统的设计,做得比它应该做的更多。
根据React的钩子文档,钩子的目的是允许您在不编写类的情况下使用状态和其他React特性。这通常被认为是设置状态、执行计算任务、以异步方式执行API或其他查询,以及响应用户输入。理想情况下,一个功能组件应该可以与一个类组件互换,但实际上,这更难实现。
创建Dropdown组件的特定解决方案虽然有效,但不是一个好的解决方案。为什么?这令人困惑,它不是不言自明的,也很难理解正在发生的事情。有了钩子,它们应该很简单,只执行一个任务,例如按钮回调处理程序,计算并返回存储的结果,或者执行通常委托给this.doSomething()
的其他任务。
返回JSX的钩子根本不是真正的钩子,它们只是功能组件,即使它们为钩子使用了正确的前缀命名约定。
React和组件更新的单向通信也存在混淆。数据的传递方式没有限制,可以用与Angular类似的方式进行处理。有mobx
这样的库,它允许您订阅和发布对共享类属性的更改,这将更新任何侦听的UI组件,该组件也可以更新它。您还可以随时使用RxJS进行异步更改,从而更新UI。
具体示例确实偏离了SOLID原则,为父组件提供了控制子组件数据的输入点。这是典型的强类型语言,如Java,在那里更难进行异步通信(现在不是真正的问题,但过去是这样)。父组件没有理由不能更新子组件——这是React的基本部分。添加的抽象越多,添加的复杂性就越高,失败点也就越多。
添加异步函数、可观察性(mobx/rxjs)或上下文的使用可以减少直接的数据耦合,但这将创建一个更复杂的解决方案。