设计动机
-
组件之间复用状态逻辑很难
-
复杂组件变得难以理解
-
难以理解的 Class
使用姿势
useState
import React from 'react';
const { useState } = React;
// https://zh-hans.reactjs.org/docs/hooks-state.html
// useState 是最简单的一个 hook
// 唯一需要注意的是不要尝试在循环或条件等不稳定的代码结构中编写
// 原因在这里 -> https://github.com/brickspert/blog/issues/26
export default function UseState() {
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
console.warn('render');
return (
<div>
useState Demo
<h4>num1:{num1}</h4>
<h4>num2:{num2}</h4>
<button onClick={() => setNum1(num1 + 1)}>add num1</button>
<button onClick={() => setNum2(n => n + 1)}>add num2</button>
</div>
);
}
useEffect
import React from 'react';
const { useState, useEffect } = React;
// https://zh-hans.reactjs.org/docs/hooks-effect.html
// https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/
// 适用场景:
// 1. 模拟钩子函数可以进行清理操作
export default function UseEffect() {
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
// 相当于 componentDidMount + componentDidUpdate + componentWillUnmount
useEffect(() => {
console.log('useEffect1');
return () => {
console.log('_useEffect1');
};
});
// 相当于 componentDidMount + componentWillUnmount
// 注意 deps 参数为空数组,不同于不传!!
useEffect(() => {
console.log('useEffect2');
return () => {
console.log('_useEffect2');
}
}, []);
// 相当于 componentDidMount + componentDidUpdate + componentWillUnmount
// 相比于第一种情况更加精确
useEffect(() => {
console.log('useEffect3');
return () => {
console.log('_useEffect3');
}
}, [num1]);
console.warn('render');
return (
<div>
useEffect Demo
<h4>{num1}</h4>
<button onClick={() => setNum1(num1 + 1)}>更新 num1</button>
<br />
<br />
<h4>{num2}</h4>
<button onClick={() => setNum2(num2 + 1)}>更新 num2</button>
</div>
);
}
useContext
import React, { useContext } from 'react';
const { useState, createContext } = React;
// https://zh-hans.reactjs.org/docs/hooks-reference.html#usecontext
// 适用场景:
// 1. 状态共享
// 唯一需要注意的是:
// 当组件上层最近的 <SizeContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 SizeContext provider 的 context value 值。
// 即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
const SizeContext = createContext({
size: 0,
// 给子组件更改 context value 值暴露接口
setSize(size: number) { console.log(size) },
});
function ChildCom() {
const { size, setSize } = useContext(SizeContext);
function updatSize() {
setSize(size + 1);
}
console.warn('child srender');
return (
<div>
<h4>子组件 size:{size}</h4>
<button onClick={updatSize}>累加</button>
</div>
)
}
export default function UseContext() {
const [size, setSize] = useState(0);
console.warn('father render');
const value = {
size,
setSize
}
return (
<div>
<SizeContext.Provider value={value}>
<h4>根组件 size:{size}</h4>
<ChildCom />
</SizeContext.Provider>
</div >
);
}
useMemo
import React from 'react';
const { useState, useMemo } = React;
// https://zh-hans.reactjs.org/docs/hooks-reference.html#usememo
// 适用场景:
// 1. 性能优化:减少不必要的重复计算
function getGreetText(name: string) {
console.log('重新计算');
return 'hello' + name;
}
// function ChildNormal(props: { name1: string, name2: string }) {
// // 不管是 name1 还是 name2 变化都会导致重新计算
// const greet = getGreetText(props.name1);
// return (
// <>
// {greet}
// </>
// );
// }
function ChildUseMemo(props: { name1: string, name2: string }) {
// 只有在 name1 变化时候才会计算
const greet = useMemo(() => getGreetText(props.name1), [props.name1]);
console.log('child render');
return (
<>
<br />
name1: {props.name1}
<br />
name2: {props.name2}
<br />
greet: {greet}
</>
);
}
export default function UseMeno() {
const [name1, setName1] = useState('');
const [name2, setName2] = useState('');
return (
<div>
name1:<input type="text" onChange={(e) => setName1(e.target.value)} />
<br />
<br />
name2:<input type="text" onChange={(e) => setName2(e.target.value)} />
<br />
<br />
{/* greet normal:<ChildNormal name1={name1} name2={name2} /> */}
<br />
<br />
greet useMemo:<ChildUseMemo name1={name1} name2={name2} />
</div>
);
}
useCallback
import React, { useCallback } from 'react';
const { useState, memo } = React;
// https://zh-hans.reactjs.org/docs/hooks-reference.html#usecallback
// useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
// 适用场景:
// 1. 性能优化:将句柄传入做了 memo 的子组件
interface IChildProps {
count: number;
onAdd(): void;
}
const Child1 = memo(function (props: IChildProps) {
console.log('Child1 Render');
return (
<div style={{ border: '1px solid #000' }}>
<h4>Child1</h4>
count:{props.count}
<button onClick={props.onAdd}>add</button>
</div>
);
});
const Child2 = memo(function (props: IChildProps) {
console.log('Child2 Render');
return (
<div style={{ border: '1px solid #000' }}>
<h4>Child2</h4>
count:{props.count}
<button onClick={props.onAdd}>add</button>
</div>
);
});
export default function UseCallback() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const onAddCount1 = () => {
setCount1(count1 + 1);
}
// 标记是稳定的,count1 变化不会影响 Child2 的渲染
const onAddCount2 = useCallback(() => {
setCount2(count2 + 1);
}, [count2]);
console.log('Wrap Renders');
return (
<div>
<Child1 count={count1} onAdd={onAddCount1} />
<br />
<Child2 count={count2} onAdd={onAddCount2} />
</div>
);
}
useLayoutEffect
import React from 'react';
const { useState, useLayoutEffect, useEffect } = React;
// https://zh-hans.reactjs.org/docs/hooks-reference.html#uselayouteffect
// 适用场景:
// 1. 解决闪烁问题
function Child1() {
const [value, setValue] = useState(0);
// 异步更新会出现闪烁
useEffect(() => {
if (value === 0) {
setValue(10 + Math.random() * 200);
}
}, [value]);
console.log('render', value);
return (
<div>
{value === 0 ? <h1>xiixix</h1> : <h4>value: {value}</h4>}
<button onClick={() => setValue(0)}>click me</button>
</div>
);
}
function Child2() {
const [value, setValue] = useState(0);
// 同步更新将不会出现闪烁
useLayoutEffect(() => {
if (value === 0) {
setValue(10 + Math.random() * 200);
}
}, [value]);
console.log('render', value);
return (
<div>
{value === 0 ? <h1>xiixix</h1> : <h4>value: {value}</h4>}
<button onClick={() => setValue(0)}>click me</button>
</div>
);
}
export default function UseLayoutEffect() {
return (
<div>
<Child1 />
<hr />
<hr />
<Child2 />
</div>
);
}
useRef
import React, { useEffect, ChangeEvent, useState } from 'react';
const { useRef } = React;
// https://zh-hans.reactjs.org/docs/hooks-reference.html#useref
export default function UseRef() {
const [num, addNum] = useState(0);
// 1. 用于获取 DOM
const ref1 = useRef<HTMLInputElement>(null);
// 2. 用作实例属性的存储,useRef 在整个组件生命周期都会保持不变
const ref2 = useRef<string>('0');
useEffect(() => {
console.log('num', ref2.current);
console.log('ref2.current', ref2.current);
});
const onClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
ref1.current?.focus();
};
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
ref2.current = e.target.value
}
console.warn('render');
return (
<div>
useRef Demo
<input ref={ref1} type="text" onChange={onChange} />
<br />
<button onClick={onClick}>Focus</button>
<br />
<button onClick={() => addNum(num + 1)}>addNum</button>
</div>
);
}
useImperativeHandle
import React from 'react';
const { useRef, useImperativeHandle, forwardRef } = React;
// https://zh-hans.reactjs.org/docs/hooks-reference.html#useimperativehandle
// 适用场景: ref 转发时候代理一层,做 API 的上层封装
interface IChildRef {
getHeight(): number;
}
const Child = forwardRef((props: {}, ref: React.Ref<IChildRef>) => {
const divRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({
getHeight: () => {
console.log('计算了高度');
return divRef.current?.clientHeight || 0;
}
}));
return (
<div ref={divRef} style={{ height: '100px', width: '100px', border: '1px solid #000' }}>
i am child
</div>
);
});
export default function UseImperativeHandle() {
const childRef = useRef<IChildRef>(null);
function getChildHeight() {
console.log(childRef.current?.getHeight());
}
return (
<div>
useImperativeHandle Demo
<Child ref={childRef} />
<button onClick={getChildHeight}>click me</button>
</div>
);
}
useReducer
import React from 'react';
const { useReducer } = React;
// https://zh-hans.reactjs.org/docs/hooks-reference.html#usereducer
// 适用场景;
// 1. 复杂的数据类型,需要差量更新
// 2. 可以获取到上一次的数据
// 3. 性能优化:稳定的 dispatch 句柄
function reducer(state: { count: number }, action: { type: 'increment' | 'decrement' }) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
export default function UseReducer() {
console.warn('render1');
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
Count: {state.count}
<br />
<br />
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
}
useContext + useReducer 实现全局共享状态
import React from 'react';
const { useReducer, createContext, useContext } = React;
type ISizeTypes = 'addWidth' | 'addHeight';
type ISize = { width: number, height: number }
const SizeContext = createContext([
{
width: 0,
height: 0
},
a => { }
]) as React.Context<[ISize, (a: { type: ISizeTypes }) => void]>;
const useSize = () => {
const [size, dispatch] = useContext(SizeContext);
function addWidth() {
dispatch({ type: 'addWidth' });
}
function addHeight() {
dispatch({ type: 'addHeight' });
}
return { size, addWidth, addHeight };
};
function reducer(state: ISize, action: { type: ISizeTypes }) {
switch (action.type) {
case 'addWidth':
return { ...state, width: state.width + 1 };
case 'addHeight':
return { ...state, height: state.height + 1 };
default:
throw new Error();
}
}
function Com1() {
const { size, addHeight, addWidth } = useSize();
return (
<div>
组件1
<br />
宽度: {size.width}
<br />
高度: {size.height}
<br />
<button onClick={addHeight}>增加高度</button>
<button onClick={addWidth}>增加宽度</button>
</div>
);
}
function Com2() {
const { size, addHeight, addWidth } = useSize();
return (
<div>
组件2
<br />
宽度: {size.width}
<br />
高度: {size.height}
<br />
<button onClick={addHeight}>增加高度</button>
<button onClick={addWidth}>增加宽度</button>
</div>
);
}
export default function UseReducer() {
console.warn('render1');
const sizeContext = useReducer(reducer, { width: 0, height: 0 });
return (
<>
<SizeContext.Provider value={sizeContext}>
<Com1 />
<hr />
<Com2 />
</SizeContext.Provider>
</>
);
}
两个原则
-
只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook
(这是为什么?) -
只在 React 函数中调用 Hook
自定义 Hooks
这部分建议直接阅读市面上一些优秀 hooks 库的源码:
参考资料: