React Hooks 的流行模式和反模式
React 引入Hooks API已经两年多了。许多项目已经采用了新的 API,并且有足够的时间来观察这些新模式在生产环境中的运作情况。在本文中,我将带您回顾我在维护一个基于 Hooks 的大型代码库后的一些经验教训。
学习 #1. 所有标准规则均适用
钩子要求开发人员学习新的模式并遵循一些钩子规则。这有时会让人觉得新模式会抛弃所有先前的良好实践。然而,钩子只是创建可重用构建块的另一种方式。如果您要创建自定义钩子,仍然需要应用基本的软件开发实践:
- 单一职责原则。一个钩子应该封装单一功能。与其创建一个超级钩子,不如将其拆分成多个更小、更独立的钩子。
- 清晰定义的 API。与普通函数/方法类似,如果一个钩子接受过多的参数,则表明该钩子需要重构以更好地封装。建议避免 React 组件包含过多的 props,React 钩子也应尽量减少参数数量。
- 可预测的行为。钩子的名称应该与其功能相对应,没有额外的意外行为。
尽管这些建议看起来非常明显,但在创建自定义钩子时遵循这些建议仍然很重要。
学习#2. 处理钩子依赖关系。
一些 React hooks 引入了“依赖项”的概念——一系列应该触发 hook 更新的元素。这在useEffect
、 和 中都很useMemo
常见useCallback
。ESLint 中有一个规则可以帮助你管理代码中的依赖项数组,但是这条规则只能检查代码结构,而不能检查你的意图。管理 hook 依赖项是最棘手的概念,需要开发人员投入大量精力。为了提高代码的可读性和可维护性,可以减少 hook 依赖项的数量。
使用这个简单的技巧,你的基于 hooks 的代码会变得更容易。例如,让我们考虑一个自定义 hook useFocusMove
:
function Demo({ options }) {
const [ref, handleKeyDown] = useFocusMove({
isInteractive: (option) => !option.disabled,
});
return (
<ul onKeyDown={handleKeyDown}>
{options.map((option) => (
<Option key={option.id} option={option} />
))}
</ul>
);
}
此自定义钩子依赖于isInteractive
,可以在钩子实现内部使用:
function useFocusMove({ isInteractive }) {
const [activeItem, setActiveItem] = useState();
useEffect(() => {
if (isInteractive(activeItem)) {
focusItem(activeItem);
}
// update focus whenever active item changes
}, [activeItem, isInteractive]);
// ...other implementation details...
}
ESLint 规则要求isInteractive
将参数添加到useEffect
依赖项中,因为规则不知道这个自定义钩子在哪里使用,也不知道这个参数是否会发生变化。然而,作为开发者,我们知道这个函数一旦定义,其实现就始终相同,将其添加到依赖项数组只会使代码变得混乱。标准的“工厂函数”模式可以解决这个问题:
function createFocusMove({ isInteractive }) {
return function useFocusMove() {
const [activeItem, setActiveItem] = useState();
useEffect(() => {
if (isInteractive(activeItem)) {
focusItem(activeItem);
}
}, [activeItem]); // no ESLint rule violation here :)
// ...other implementation details...
};
}
// usage
const useFocusMove = createFocusMove({
isInteractive: (option) => !option.disabled,
});
function Demo({ options }) {
const [ref, handleKeyDown] = useFocusMove();
// ...other code unchanged...
}
这里的技巧是将运行时参数和开发时参数分开。如果在组件生命周期内某些东西发生了变化,它就是运行时依赖项,并会被添加到依赖项数组中。如果组件的依赖项一旦确定,并且在运行时永远不会改变,那么尝试工厂函数模式是一个好主意,它可以简化 hooks 依赖项的管理。
学习 #3. 重构 useEffect
useEffect
钩子函数为我们在 React 组件内部实现命令式 DOM 交互提供了一个平台。有时,组件可能会变得非常复杂,而在其上添加依赖项数组会使代码的阅读和维护更加困难。这可以通过将命令式 DOM 逻辑提取到钩子代码之外来解决。例如,考虑一个钩子useTooltipPlacement
:
function useTooltipPosition(placement) {
const tooltipRef = useRef();
const triggerRef = useRef();
useEffect(() => {
if (placement === "left") {
const triggerPos = triggerRef.current.getBoundingElementRect();
const tooltipPos = tooltipPos.current.getBoundingElementRect();
Object.assign(tooltipRef.current.style, {
top: triggerPos.top,
left: triggerPos.left - tooltipPos.width,
});
} else {
// ... and so on of other placements ...
}
}, [tooltipRef, triggerRef, placement]);
return [tooltipRef, triggerRef];
}
如果正确使用钩子依赖项,内部代码useEffect
会变得非常长,难以跟踪。为了简化操作,我们可以将效果内容提取到一个单独的函数中:
// here is the pure DOM-related logic
function applyPlacement(tooltipEl, triggerEl, placement) {
if (placement === "left") {
const triggerPos = tooltipEl.getBoundingElementRect();
const tooltipPos = triggerEl.getBoundingElementRect();
Object.assign(tooltipEl.style, {
top: triggerPos.top,
left: triggerPos.left - tooltipPos.width,
});
} else {
// ... and so on of other placements ...
}
}
// here is the hook binding
function useTooltipPosition(placement) {
const tooltipRef = useRef();
const triggerRef = useRef();
useEffect(() => {
applyPlacement(tooltipRef.current, triggerRef.current, placement);
}, [tooltipRef, triggerRef, placement]);
return [tooltipRef, triggerRef];
}
我们的钩子现在只有一行,而且很容易跟踪依赖关系。此外,我们还获得了一个纯 DOM 的定位实现,可以在 React 之外使用和测试 :)
学习 #4. useMemo、useCallback 和过早优化
你可以依赖 useMemo 进行性能优化
出于某种原因,开发人员会把这部分理解成“你必须”而不是“你可以”,并试图记住所有内容。乍一看,这似乎是个好主意,但具体细节就比较棘手了。
为了充分利用记忆化机制,需要使用React.memo
或PureComponent
包装器来防止组件被不必要的更新。此外,还需要进行非常精细的调整和验证,以确保所有属性的更改频率都不超过应有的水平。任何一个错误的属性都可能像纸牌屋一样破坏所有记忆化机制:
现在是回顾 YAGNI 方法并将记忆化工作集中在应用程序中几个最常用的位置的好时机。在代码的其余部分,不值得使用useMemo
/增加额外的复杂性useCallback
。您可以使用普通函数编写更简单易读的代码,并在其优势更加明显时再应用记忆化模式。
在进行记忆化之前,我还建议您查看文章“在使用 memo() 之前”,您可以在其中找到一些记忆化的替代方法。
学习 #5. 其他 React API 仍然存在
如果你有一把锤子,所有东西看起来都像钉子
Hooks 的引入使得一些其他 React 模式变得过时。例如,useContext
Hook 似乎比Consumer 组件更方便。
然而,其他 React 特性仍然存在,不应被遗忘。例如,让我们来看看这个钩子代码:
function useFocusMove() {
const ref = useRef();
useEffect(() => {
function handleKeyDown(event) {
// actual implementation is extracted outside as shown in learning #3 above
moveFocus(ref.current, event.keyCode);
}
ref.current.addEventListener("keydown", handleKeyDown);
return () => ref.current.removeEventListener("keydown", handleKeyDown);
}, []);
return ref;
}
// usage
function Demo() {
const ref = useFocusMove();
return <ul ref={ref} />;
}
这看起来像是一个合适的 Hooks 用例,但为什么我们不能将实际的事件订阅委托给 React,而不是手动操作呢?这里有一个替代方案:
function useFocusMove() {
const ref = useRef();
function handleKeyDown(event) {
// actual implementation is extracted outside as shown in learning #3 above
moveFocus(ref.current, event.keyCode);
}
return [ref, handleKeyDown];
}
// usage
function Demo() {
const [ref, handleKeyDown] = useFocusMove();
return <ul ref={ref} onKeyDown={handleKeyDown} />;
}
新的钩子实现更短,并且具有优势,因为钩子消费者现在可以决定在哪里附加侦听器,以防他们有更复杂的 UI。
这只是一个例子,可能还有许多其他场景,但主要观点仍然是一样的——即使有钩子可用,许多 React 模式(高阶组件、渲染 props 等)仍然存在并且有意义。
结论
基本上,以上所有学习都围绕着一个基本原则:保持代码简短易读。这样,你将来就能扩展和重构它。遵循标准的编程模式,你基于钩子的代码库就能长久繁荣。
文章来源:https://dev.to/justboris/popular-patterns-and-anti-patterns-with-react-hooks-4da2