React Hook
React Hook 的基础知识
React Hooks
React Hook 是用来实现逻辑的复用的加强版的函数组件,使用 hook 实现逻辑复用可以避免使用 render props 和 高阶组件多次嵌套形成的“嵌套地狱”。
优点
React 高阶组件造成的嵌套地狱是指当使用多个高阶组件包装一个组件时,会产生很多层级的容器组件,导致代码可读性和维护性降低,例如,如果一个组件被这样包装:
export default withSubscription(withMouse(withCat(WrappedComponent)));
那么在 React Developer Tools 中,它的显示名称可能是这样的:
<WithSubscription> <WithMouse> <WithCat> <WrappedComponent /> </WithCat> </WithMouse> </WithSubscription>
类组件复用逻辑需要用到高阶函数,高阶函数是一个黑盒,外面的组件不知道内部的组件的逻辑,层层叠加之后,会给调试带来困难,而 hook 能够抽离逻辑到组件外部,很好的解决了这个问题;
类组件因为需要创建类组件的实例,所以对性能的开销较大,函数组件开销相对较小;
相较于也是复用逻辑的 HOC,每次调用高阶组件生成的都是是一个全新的组件,组件的唯一标识响应的也会改变,如果在render方法调用了高阶组件,这会导致组件每次都会被卸载后重新挂载。
函数组件不存在 state,但是 hook 弥补了这个缺点;
限制
不要在循环、条件判断或嵌套函数中使用(或者说初始化) Hooks,例如:
if(exsit){ const [name,setName] = useState(""); }
因为 Hooks 是基于闭包实现,在调用时每个 hook 都按顺序加入链表结构中,并赋予相对应的 index,在更新状态时,会根据 index 去修改 state,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。
只能在函数组件中使用 Hooks
useEffect
source 参数
useEffect 包含了 componentDidMount、componentDidUpdate、componentWillUnmount 三个生命周期,区别这三个生命周期主要是靠 source 参数:
useEffect(() => {
console.log('componentDidMount || componentDidUpdate');
return () => {
console.log('componentWillUnmount');
}
}, source);
当 source 没有值的时候,相当于 componentDidUpdate,每次更新都会被调用,且先调用 return 的函数,再调用外部的函数;
当 source 为空数组时,外部函数相当于 componentDidMount,return 函数相当于 componentWillUnmount ;
当 source 有值时,相当于 componentDidUpdate,但只会在 source 数组中的值变化时才会被调用,也是先调用 return 的函数,再调用外部的函数;
回调函数
useEffect 的回调函数,是对上一次调用 useEffect 进行清理,举个例子:
export default function HookTest() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('执行...', count);
return () => {
console.log('清理...', count);
}
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => { setCount(count + 1) }}>
Click me
</button>
</div>
);
}
// output:
// 执行... 0
// 清理... 0 , 执行... 1
// 清理... 1 , 执行... 2
可以看到,在发生 state 变动的时候,会先触发 return 内的函数,且 return 内的函数获取到的是之前的 state。
那么再来看一下,useEffect 和渲染的执行顺序:
export default function HookTest() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('执行...', count);
return () => {
console.log('清理...', count);
}
}, [count]);
return (
<div>
{console.log(`render ${count}`)}
<p>You clicked {count} times</p>
<button onClick={() => { setCount(count + 1) }}>
Click me
</button>
</div>
);
}
// output:
// render 0 , 执行... 0
// render 1 , 清理... 0 , 执行... 1
// render 2 , 清理... 1 , 执行... 2
可以看到,render 要优先于 useEffect 触发。
useState
实现累加:将函数作为值传入,可以获取到上一次修改的 state,然后返回一个更新后的值:
setTranslateY(prevState => prevState + 1);
useLayoutEffect
useLayoutEffect 的作用和 useEffect 类似,写法上也类似:
useEffect(() => {
// 执行副作用
return () => { /* clean up */}
}, [dependency, arr])
useLayoutEffect(() => {
// 执行副作用
return () => { /* clean up */}
}, [dependency, array])
他们的不同点在于:
useEffect 是在渲染函数执行完成,并绘制到屏幕之后,再异步执行。大概流程如下:
- 触发渲染函数执行(改变状态,或者父组件重新渲染)
- React调用组件的渲染函数
- 屏幕中重绘完成
- 执行useEffect
useLayoutEffect 是在渲染函数执行之后,但是屏幕重绘前同步执行。大概流程如下:
- 触发渲染函数执行(改变状态,或者父组件重新渲染)
- React调用组件的渲染函数
- 执行 useLayoutEffect,并且 React 会等待它执行完成
- 屏幕中重绘完成
如何选择使用 useEffect 还是 useLayoutEffect?
useLayoutEffect 优先于 useEffect 触发,且在渲染完成之前触发,所以如果执行的操作会引起页面的闪烁,可以优先使用 useLayoutEffect,但是 useLayoutEffect 是同步执行的,会阻塞渲染,所以没有特殊需求的操作尽量使用 useEffect
useId
如果应用是 CSR(客户端渲染),id 是稳定的,不需要使用该 API,但如果应用是 SSR(服务端渲染),那么App.tsx 会经历:
React 在服务端渲染,生成随机id(假设为0.1234),这一步叫 dehydrate(脱水)
React 在客户端渲染,生成随机id(假设为0.6789),这一步叫 hydrate(注水)
最后客户端、服务端生成的 id 会出现不匹配的情况,于是 React 18 中新增了 useId 来生成不变的 ID
function Checkbox() {
const id = useId();
return (
<>
<label htmlFor={id}>Do you like React?</label>
<input id={id} type="checkbox" name="react"/>
</>
);
};
它的本质是通过组件在树中的层级计算出唯一ID,如下图所示
useTransition
当函数被 startTransition 包裹时,函数 setState 触发的渲染就会被标记为不紧急渲染,有可能被其他的紧急渲染所挤占,举个例子:
function App() {
const [isPending, startTransition] = useTransition();
const [count, setCount] = useState(0);
function handleClick() {
startTransition(() => {
setCount(c => c + 1); // 此处为不紧急渲染,如果有文本输入等紧急渲染任务,此渲染会被推迟
})
}
return (
<div>
{isPending && <Spinner />}
<button onClick={handleClick}>{count}</button>
</div>
);
}
useDeferredValue
其作用于 useTransition 类似,不过 useTransition 是包装更新任务成为延迟更新任务,而 useDeferredValue 是产生一个新的值,将这个值延迟。
useDeferredValue 仅延迟你传递给它的值。如果你想要在紧急更新期间防止子组件重新渲染,则还必须使用 React.memo 或 React.useMemo 记忆该子组件:
function Typeahead() {
const query = useSearchQuery('');
const deferredQuery = useDeferredValue(query);
// Memoizing 告诉 React 仅当 deferredQuery 改变,
// 而不是 query 改变的时候才重新渲染
const suggestions = useMemo(() =>
<SearchSuggestions query={deferredQuery} />,
[deferredQuery]
);
return (
<>
<SearchInput query={query} />
<Suspense fallback="Loading results...">
{suggestions}
</Suspense>
</>
);
}
自定义 Hook 实现逻辑复用
自定义 hook 是 Hook 模式下实现 render props 或者高阶组件的方式,举个例子:
// useSetOnlineState.jsx
import { useState, useEffect } from 'react';
export const useIsOnline = (userInfo) => {
const [isOnline,setIsOnline] = useState(null);
useEffect(()=>{
setIsOnline( useInfo.state === 'online' );
});
return isOnline;
}
上例新建了一个 useIsOnline 的 hook,根据传入的 userInfo 来判断并设置内部状态 isOnline 并将其返回,下面是该 hook 的调用方式:
function UserList(props) {
const isOnline = useIsOnline( userItem.userInfo );
return ( ... );
}
自定义 hook 除了可以直接返回值外,还可以返回设置默认值、内部状态的方法,这样实现的 hook 使用方法与原生 useState 相同:
// useRandomColor.jsx
import { useState, useEffect } from 'react';
export const useRandomColor = (initialColor) => {
const [color,setColor] = useState(initialColor);
const changeColor = (currentColor) =>{
setColor(currentColor);
}
return [ color,changeColor ];
}
Hook 父组件调用子组件方法
首先在父组件中引入子组件,并父给子组件一个 ref 对象:
const childrenRef = useRef(null);
return <children ref={childrenRef} />
然后修改子组件,给子组件套上forwardRef
,然后使用useImperativeHandle
暴露 ref 自定义的实例值给父组件:
function children(props, ref) {
useImperativeHandle(ref, () => ({
hello: () => console.log('hello world!')
}))
return <h1>children</h1>
}
export default forwardRef(children);
最后在父组件中调用:
const something = () => childrenRef.current.hello();