背景
由于新同学对 React hook
不熟悉,需要老同学的帮助。因此整理分享下hook
相关的入门经验,希望对新同学有所帮助。
开始之前: 新同学首先要了解下官方文档。
官网将 hook 区分为基础 hook
和 额外的 hook
。下面基于个人开发经验对hook
做了更细致的分类。
hooks 简介
-
状态管理
- useState
管理组件状态
- useReducer
同 useState ,但是可以定制更新状态的逻辑
- useContext
跨组件管理状态
- useState
-
副作用
- useEffect
绑定函数是否重新执行和依赖的数据
- useLayoutEffect
通 useEffect,但是执行函数的时机为 dom 更新后
- useEffect
-
性能优化
- useMome
缓存计算结果
- useCallback
缓存函数
- useMome
-
额外的 hook
- useRef
获取dom、获取子组件方法或保存数据
- useImperativeHandle
将子组件方法暴露给父级
- useRef
正文
分享的思路:
基于一个实际场景的功能实现,期望同学可以理解什么场景
使用什么hook
。
案例的基本功能如图:
一个列表页。
获取列表数据
useEffect
一般会作为数据请求使用到的 hook。但是我们获取数据一般为一个async
函数,这里推荐使用 ahooks
的 useAsyncEffect
import { useState, useEffect } from "react";
let times = 0;
const data = [
{
text: "第一条"
},
{
text: "第二条"
}
];
const getList = () => {
times++;
return data.map((item) => ({
text: `${item.text} - ${times}`,
}));
};
const Item = ({ idx, text, onMount }) => {
console.log(`item ${idx} 渲染`);
return <li>{text}</li>;
};
const List = () => {
const [list, setList] = useState([]);
const [parentState, setParentState] = useState(0);
const setListByRequest = useCallback(() => {
const data = getList();
setList(data);
}, [])
useEffect(() => {
setListByRequest();
}, [setListByRequest]);
const onMount = () => {}
return (
<>
<span>{parentState}</span>
<button onClick={() => setParentState(oldV => (oldV + 1))}>更新状态</button>
<ul>
{list.map(({ text }, idx) => {
return <Item key={idx} idx={idx} onMount={onMount} text={text} />;
})}
</ul>
</>
);
};
export default function App() {
return (
<div className="App">
<List />
</div>
);
}
useEffect 延伸
我们在使用 useEffect
做接口请求时,会把接口参数胡作为依赖项。
以分页请求为例: useEffect(updateFn, [pageNum, pageSize])
。
如果从 [1, 10] 变更为 [2, 10] 则 updateFn 执行
但一般我们维护接口请求的参数都是一个对象
{ pageNum: 1, pageSize: 10 }
useEffect
不会比较对象内的变量是否有变更。因此我们可以封装下 useEffect
的比较逻辑。
import { useEffect, useRef } from 'react'
import { isEqual } from 'lodash'
const useDeepDiffEffect = (updateCallback, deps) => {
// useRef 可以用来保存数据
// 只有通过 xxRef.current = xxx 才会影响数据
const oldDepsRef = useRef()
const updateFlagRef = useRef(false)
// 使用 isEqual 深度比较对象内的数据是否有变化
if (!isEqual(oldDepsRef.current, deps) {
updateFlagRef = !updateFlagRef.current
oldDepsRef.current = deps
}
useEffect(updateCallback, [updateFlagRef.current])
}
减少 Item 渲染
List
组件在渲染时,Item
也会随之渲染。理论上讲,应该只有在列表数据变更时,Item
才去重新渲染。
目前案例中,点击 更新状态 这个按钮。触发setParentState
后,我们就可以看到Item
里面的console.log
信息。但实际我们并没有更改list
里面的数据。
下面就用需要用到React.mome
和useCallback
(上面有提到)。
这里介绍下React.memo
:
在参数
props没有变更时避免组件的重新渲染
官方文档
更改上方Item
的代码
const Item = React.memo(({ idx, text, onMount }) => {
console.log(`item ${idx} 渲染`);
return <li>{text}</li>;
});
使用React.memo
后,我们就需要保证props
内的每个对象引用都是不变的。
但是在父组件每次从新渲染时,onMount
每次都会再声明一次,因此传递给Item
的props
每次也会不一样。
因此我们可以使用 useCallback
包裹onMount
函数,保证onMount
的引用维持不变,
const onMount = useCallback(() => {}, [])
现在再点击 更新状态 这个按钮。 我们现在就看不到Item
里面的console.log
信息了。
useCallback 延伸
useCallback
传递的 callback
函数,如果内部使用到了某一个 state
,而没有声明对state
的依赖。则callback
执行时,获取到的state
就可能是旧值。下面用一段代码解释下原因。
错误的场景
import { useEffect, useState, useCallback } from 'react'
function App () {
const [state, setState] = useState(0)
const logState = useCallback(() => {
console.log(state)
}, [])
useEffect(() => {
// 这里更新 state 为 1
setState(1)
}, [])
useEffect(() => {
setTimeout(() => {
// 这里打印的 state 为 0
logState()
}, 1000)
}, [logState])
}
下面模拟下造成上面问题的原因:
let oldCallback = null;
let oldDeps = null;
const useCallback = (callback, deps = []) => {
if (!oldCallback || !oldDeps || !oldDeps.every((item, idx) => item === deps[idx])) {
oldCallback = callback;
oldDeps = deps;
}
return oldCallback;
};
const data = {
a: 1
}
function render() {
let a = data.a
console.log('实际值--->', a)
const fn = useCallback(() => {
console.log('useCallback值--->', a)
}, []); // 对比这里我们加入 a 这个依赖
fn()
}
render();
// 更新变量值
data.a = data.a + 1
render()
详细原因可参考维基百科-静态作用域、MDN-闭包
主动刷新List
List
依赖的数据,是在组件内部发送请求获取到的。如果我们在 List
外, 因为一些特殊原因需要主动刷新List
,就需要将List
的setListByRequest
方法暴露外部。类似的场景譬如Antd ProComponent
的ProTable
手动触发
下面就用需要用到React.forwardref
、useRef
和useImperativeHandle
。
这里介绍下React.forwardref
:
可以接收组件的
ref
属性。官方文档
改造上面的代码
import Reaact, { useRef, useImperativeHandle } from 'react'
const List = React.forwardref((props, ref) => {
....
useImperativeHandle(ref, () => ({
reload: () => {
setListByRequest();
}
}));x
...
})
export default function App() {
const listRef = useRef();
const onReload = () => {
listRef?.current?.reload?.();
};
return (
<div className="App">
<div>
<button onClick={onReload}>刷新列表</button>
</div>
<hr />
<List ref={listRef} />
</div>
);
}
主题色
主题色是整个应用的状态,因此很容我们就会想到使用useContext
。useContext
是用来消费全局状态的,而创建就需要使用React.createContext
。类似的场景如antd
的ConfigProvider 全局配置
这里介绍下React.createContext
:
创建一个可供子组件消费的上下文。 官方文档
React.createContext
方法返回一个值中,有个Provider
的属性。此属性是一个组件,接收value
属性作为全局的状态。如果某个组件使用useContext
消费某个Context
,则需要保证该组件被Provider
包裹。
改造上面的代码
import Reaact, { useContext } from 'react'
const GlobalContext = React.createContext({});
const Item = React.memo(({ idx, text, onMount }) => {
const themeConfig = useContext(GlobalContext);
console.log(`item ${idx} 渲染`);
return <li style={{
color: themeConfig.color
}}>{text}</li>;
});
export default function App() {
...
const [theme, setTheme] = useState("black");
...
return (
<GlobalContext.Provider
value={{
color: theme
}}
>
<div className="App">
<div>
<button onClick={onReload}>刷新列表</button>
<button onClick={() => setTheme("black")}>黑色主题</button>
<button onClick={() => setTheme("blue")}>蓝色主题</button>
</div>
<hr />
<List ref={listRef} />
</div>
</GlobalContext.Provider>
);
}
完整的Demo