前面的话
上篇介绍了 useState
和 useEffect
两个钩子函数,这篇将接着介绍其他常用的钩子函数。
1、useCallback
作用:
- 该 hooks 返回一个 memoized 回调函数,根据依赖项来决定是否更新函数。
- 其依赖项可以为一个空数组, 表示没有依赖值, 将永远不会被更新。
- useCallback 可以优化代码,防止不必要的渲染。但也并非所有的方法都要用 useCallback 去包裹一层。
使用例子
useCallback
与 React.memo
完美搭配:
React.memo
的作用:只适用于函数组件,能对 props 做浅比较,可以防止组件无效的重复渲染。
场景: 一个父组件,包含一个子组件,子组件接受一个函数作为 props;一般来说,如果父组件更新了,子组件也会更新,但大多数情况,子组件的更新是没有必要的,可以借助 useCallback 来返回一个函数,然后把这个函数作为 props 传递给子组件;这样子组件就可以避免不必要的更新渲染。
interface Props {
onClickButton: () => void;
children: string;
}
// 父组件
const Parent = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const handleClickButton1 = () => {
setCount1(count1 + 1);
};
const handleClickButton2 = useCallback(() => {
setCount2(count2 + 1);
}, [count2]);
return (
<>
<div>
<Button onClickButton={handleClickButton1}>Button1</Button>
</div>
<div>
<Button onClickButton={handleClickButton2}>Button2</Button>
</div>
</>
);
};
// 子组件
const Button = React.memo(({onClickButton, children}: Props) => {
return (
<>
<button onClick={onClickButton}>{children}</button>
<span>{Math.random()}</span>
</>
);
});
点击上述两个按钮会发现,点击 Button1 时只会更新Button1 后面的nearing,点击 Button2 时,两个按钮后面的内容都更新了。
为什么点击 Button2 时也会导致 Button1 后面的内容更新,尽管子组件使用了React.memo也无效。
那是由于 React.memo 是浅比较, 当点击 Button2 时导致父组件重新渲染,handleClickButton1这个函数虽与渲染前一样没有发生变化,但是两个一样的函数是不想等的,这样 React.memo 在比较是发现两个函数不想等,所以还是会重新更新。
为什么点击 Button1 时不会重新渲染 Button2 的内容?
由于 handleClickButton2 函数使用 useCallback 包裹,并且还依赖 count2 这个变量,这里 useCallback 会根据 count2 是否发生变化,从而决定是否返回一个新的函数,函数内部作用域也会随之跟新。所以点击 Button1 不会重新渲染 Button2 的内容。
2、useMemo
useCallback 是用来返回一个 memoized 函数,而 useMemo 用来返回一个 memorized 的值。
作用:
- 用于缓存昂贵的计算,避免在每次渲染时都进行高开销的计算
- 减少子组件不必要的渲染
- 并非所有的变量值都采用 useMemo 做缓存优化
使用例子1
缓存昂贵的计算:
const num = useMemo(()=> {
let num;
// 根据 defaultValue 来对 num 进行复杂的计算
//...
return num;
},[defaultValue])
使用例子2
useMemo 与 React.memo 完美搭配:
interface Data {
name: string;
}
interface Props {
data: Data;
}
// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
const [name, setName] = useState('xiaoqi');
const handleClickButton1 = () => {
setCount(count + 1);
};
const handleClickButton2 = (name: string) => {
setName(name);
};
const data = {
name,
};
return (
<>
<div>
<p>{count}</p>
<button onClick={handleClickButton1}>update count</button>
<button onClick={() => handleClickButton2('rose')}> update name</button>
</div>
<Child data={data} />
</>
);
};
// 子组件
const Child = React.memo(({data}: Props) => {
return (
<>
<p>child</p>
<p>
{data.name} {Math.random()}
</p>
</>
);
});
可以发现,当点击按钮1时,子组件也会重新渲染。原因就是 React.memo 是浅比较,而每次父组件渲染后都会又一个新的对象data,尽管只一样,但是地址不一样,还是会重新渲染子组件。
将 data 使用 useMemo 来包裹, 依赖值是 name ,这样点击按钮1,子组件不会重新渲染。
const data = useMemo(() => {
return {
name,
};
},[name]);
⚠️注意:
-
不要过度依赖 useMemo,useMemo 本身有一定的开销。useMemo会记住一些值,在后续的render中,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回所记住的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useMemo 可能会影响程序的性能。
-
使用 useMemo 前,应该考虑:
1、传递给 useMemo 的函数开销大不大? 如果计算开销很大,我们就要记住它的返回值。但对于简单的 js 运算,不需要使用 useMemo 来记住它的返回值。
2、如果返回值是引用类型,并且它被当作props传递给子组件,那么需要使用 useMemo 来记住这个返回值。否则尽管这个引用类型的值没有发生变化,但是地址会发生变化,React.memo 也会判断其不相等。
3、useRef
useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数。返回的对象在组件的整个周期内保持不变。
useRef 创建的对象与自建的 {current: …} 对象区别是每次渲染时,useEef 返回的是同一个对象。
const testRef = useRef(initialValue);
作用:
- 获取 Dom 元素的节点
- 获取组件实例
- 渲染周期之后共享数据的存储
使用例子1:
interface ChildProps {
num: string;
}
class Child extends React.Component<ChildProps> {
render() {
const {num} = this.props;
return <h2>count: {num}</h2>;
}
}
const Example = () => {
const [count, setCount] = useState(0);
const inputRef = useRef(null);
const childRef = useRef(null);
useEffect(() => {
console.log(inputRef.current);
console.log(childRef.current);
}, []);
return (
<div>
<input ref={inputRef} type='text' />
<p>{count}</p>
<button onClick={() => setCount(count => count + 1)}>setCount</button>
<Child num='1' ref={childRef} />
</div>
);
};
上面的例子中获取 Dom 元素的节点和组件实例。这里子组件使用 class 写法的原因是:函数组件没有实例,如果想使用 ref 来获取组件实例,子组件就要使用 class 的形式。
使用例子2
对于 useState 我们知道,每次 set 之后就会重新渲染组件,但如果我们想在 set 之后,立即获得新的 state ,我们可以使用 useRef 来保存、修改、读取。
const Example = () => {
const [count, setCount] = useState(0);
const countRef = useRef(0);
const childRef = useRef(null);
useEffect(() => {
console.log(childRef.current);
}, []);
const handleClick = () => {
const newCount = count + 1;
setCount(newCount);
countRef.current = newCount;
};
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>setCount</button>
<Child num='1' ref={childRef} />
</div>
);
};
如果我们想获取子组件里的 Dom 元素节点, 可以使用 React.forwardRef:
const Child = React.forwardRef(
(
props: ChildProps,
ref:
| string
| ((instance: HTMLHeadingElement | null) => void)
| React.RefObject<HTMLHeadingElement>
| null
| undefined
) => {
const {num} = props;
return <h2 ref={ref}>count: {num}</h2>;
}
);
const Example = () => {
const [count, setCount] = useState(0);
const countRef = useRef(0);
const childRef = useRef(null);
useEffect(() => {
console.log(childRef.current);
}, []);
const handleClick = () => {
const newCount = count + 1;
setCount(newCount);
countRef.current = newCount;
};
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>setCount</button>
<Child num='1' ref={childRef} />
</div>
);
};
此时 childRef.current 指向的是子组件中的 DOM 元素 H2:
4、useContext
const MyContext = React.createContext();
const value = useContext(MyContext);
- useContext 接受一个 context 对象(React.createContext 的返回值) 并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
- CreateContext 接受一个参数为 value prop 的初始值。
5、useReducer
useReducer 是 useState 的替代方案,当需要管理比较复杂的状态时,使用 useReducer 才是最优解。
useReducer 接受一个形如 (state, action) => newState 的 reducer, 并返回当前的 state与dispatch 方法。
const [state, dispatch] = useReducer(reducer, initialArg, init);
使用例子
interface InitState {
count: number;
}
type ActionType =
| {
type: 'increment';
}
| {
type: 'decrement';
};
const initCount = { count: 0 };
const counterReducer = (state: InitState, action: ActionType) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
};
const Counter = () => {
const [countState, dispatch] = useReducer(counterReducer, initCount);
return (
<div>
<p>count: {countState.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
</div>
);
};
export default Counter;