Hooks在React 16.8发布时突然出现,其崇高目标是改变我们编写React组件的方式。尘埃落定,Hooks已经很普遍了。Hooks成功了吗?
最初的市场宣传将Hooks作为一种摆脱类组件的方式。类组件的主要问题是,可组合性很困难。重新分享包含在生命周期事件中的逻辑componentDidMount
和朋友,导致了诸如高阶组件和 [renderProps](https://reactjs.org/docs/render-props.html)
这些都是有边缘情况的尴尬模式。钩子最好的地方是它们能够隔离交叉的关注点,并且是可组合的。
好的
Hooks做得好的地方是封装状态和共享逻辑。库包,如 [react-router](https://reactrouter.com/web/guides/quick-start)
和 [react-redux](https://react-redux.js.org/)
等库包因为Hooks而拥有更简单、更干净的API。
下面是一些使用老式的connect
API的示例代码。
import React from 'react';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { AppStore, User } from '../types';
import { actions } from '../actions/constants';
const mapStateToProps = (state: AppStore) => ({
users: state.users
});
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
addItem: (user: User) => dispatch({ type: actions.ADD_USER, payload: user })
}
}
const UsersContainer: React.FC<{users: User[], addItem: (user: User) => void}> = (props) => {
return (
<>
<h1>HOC connect</h1>
<div>
{
users.map((user) => {
return (
<User user={user} key={user.id} dispatchToStore={props.addItem} />
)
})
}
</div>
</>
)
};
export default connect(mapStateToProps, mapDispatchToProps)(UsersContainer);
复制代码
像这样的代码是臃肿和重复的。输入mapStateToProps
和mapDispatchToProps
是很烦人的。
下面是同样的代码,经过重构后使用Hooks。
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { AppStore, User } from '../types';
import { actions } from '../actions/constants';
export const UsersContainer: React.FC = () => {
const dispatch = useDispatch();
const users: User[] = useSelector((state: AppStore) => state.users);
return (
<>
<h1>Hooks</h1>
{
users.map((user) => {
return (
<User user={user} key={user.id} dispatchToStore={dispatch} />
)
})
}
</>
)
};
复制代码
差异是夜以继日的。钩子提供了一个更干净、更简单的API。钩子还消除了将所有东西包在一个组件中的需要,这是另一个巨大的胜利。
糟糕的是
依赖性阵列
useEffect
钩子需要一个函数参数和一个依赖数组作为第二个参数。
import React, { useEffect, useState } from 'react';
export function Home() {
const args = ['a'];
const [value, setValue] = useState(['b']);
useEffect(() => {
setValue(['c']);
}, [args]);
console.log('value', value);
}
复制代码
上面的代码将导致useEffect
钩子无限地旋转,因为这个看似无辜的分配。
const args = ['a'];
复制代码
在每次新的渲染中,React都会保留一份来自上一次渲染的依赖性数组的副本。React会将当前的依赖性数组与之前的数组进行比较。每个元素都会使用Object.is
方法进行比较,以确定useEffect
是否应该用新的值再次运行。对象是通过引用而不是通过值进行比较的。变量args
,在每次重新渲染时都会成为一个新的对象,并且在内存中的地址与上次不同。
突然间,变量赋值会有隐患。不幸的是,围绕着依赖阵列还有很多很多类似的陷阱。创建一个内联的箭头函数,最后进入依赖数组将导致同样的命运。
解决办法当然是使用更多的Hooks。
import React, { useEffect, useState, useRef } from 'react';
export function Home() {
const [value, setValue] = useState(['b']);
const {current:a} = useRef(['a'])
useEffect(() => {
setValue(['c']);
}, [a])
}
复制代码
将标准的JavaScript代码包装成大量的 “Hooks”,会变得很混乱和笨拙。 [useRef](https://blog.logrocket.com/usestate-vs-useref/)
, [useMemo](https://blog.logrocket.com/rethinking-hooks-memoization/)
,或 [useCallback](https://blog.logrocket.com/react-usememo-vs-usecallback-a-pragmatic-guide/)
钩子。eslint-plugin-react-hooks插件做了一个合理的工作,使你保持直线和狭窄,但bug并不少见,ESLint插件应该是一个补充,而不是强制性的。
丑陋的
我最近发布了一个 react Hook,react-abortable-fetch,用useRef
,useCallback
, 或useMemo
的组合来包装所有东西,这不是一个好的体验。
const [machine, send] = useMachine(createQueryMachine({ initialState }));
const abortController = useRef(new AbortController());
const fetchClient = useRef(createFetchClient<R, T>(builderOrRequestInfos, abortController.current));
const counter = useRef(0);
const task = useRef<Task>();
const retries = useRef(0);
const timeoutRef = useRef<number | undefined>(timeout ?? undefined);
const accumulated = useRef(initialState);
const acc = accumulator ?? getDefaultAccumulator(initialState);
const abortable = useCallback(
(e: Error) => {
onAbort(e);
send(abort);
},
[onAbort, send],
);
// etc.
复制代码
由此产生的依赖性数组相当大,并且需要随着代码的改变而不断更新,这很烦人。
}, [
send,
timeout,
onSuccess,
parentOnQuerySuccess,
parentOnQueryError,
retryAttempts,
fetchType,
acc,
retryDelay,
onError,
abortable,
abortController,
]);
复制代码
最后,我不得不小心翼翼地使用useMemo
,将Hook函数的返回值备忘化,当然,还要兼顾另一个依赖性数组。
const result: QueryResult<R> = useMemo(() => {
switch (machine.value as FetchStates) {
case 'READY':
return {
state: 'READY',
run: runner,
reset: resetable,
abort: aborter,
data: undefined,
error: undefined,
counter: counter.current,
};
case 'LOADING':
return {
state: 'LOADING',
run: runner,
reset: resetable,
abort: aborter,
data: undefined,
error: undefined,
counter: counter.current,
};
case 'SUCCEEDED':
return {
state: 'SUCCEEDED',
run: runner,
reset: resetable,
abort: aborter,
data: machine.context.data,
error: undefined,
counter: counter.current,
};
case 'ERROR':
return {
state: 'ERROR',
error: machine.context.error,
data: undefined,
run: runner,
reset: resetable,
abort: aborter,
counter: counter.current,
};
}
}, [machine.value, machine.context.data, machine.context.error, runner, resetable, aborter]);
复制代码
执行顺序
正如 “Hooks规则 “中所说,Hooks每次都需要以相同的顺序运行。
不要在循环、条件或嵌套函数内调用Hooks。
似乎相当奇怪的是,React的开发者并没有想到会在事件处理程序中执行Hooks。
通常的做法是,从Hook中返回一个可以在Hooks顺序之外执行的函数。
const { run, state } = useFetch(`/api/users/1`, { executeOnMount: false });
return (
<button
disabled={state !== 'READY'}
onClick={() => {
run();
}}
>
DO IT
</button>
);
复制代码
判决结果
前面提到的对react-redux
代码的简化是令人信服的,并带来了极好的净代码减少。Hooks需要的代码比以前的现任者要少,仅这一点就应该使Hooks成为不二之选。
Hooks的优点多于缺点,但这并不是压倒性的胜利。Hooks是一个优雅而聪明的想法,但在实践中使用起来却很有挑战性。手动管理依赖关系图和在所有正确的地方进行备忘可能是大多数问题的来源,这一点可以重新考虑一下。生成器函数可能更适合于此,因为它具有美丽的、独特的暂停和恢复执行的能力。
闭包是问题和陷阱的发源地。一个陈旧的闭包可能会引用那些没有更新的变量。对闭包的了解是使用Hooks时的一个障碍,你必须用这些知识来进行调试。
The postReact Hooks:好的、坏的和丑的,首先出现在LogRocket博客上。