前面的话
实习开始没多久,就开始帮着做一个管理系统的项目, 项目技术战使用 react + ts + antd。在三者之前都没接触的情况下,我硬着头皮上了,class 组件都还没搞明白,就开始了 hooks 之旅。。。
1、Hooks
React Hooks 的意思是: 组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。
React Hooks 就是那些钩子,需要什么功能,就使用什么功能。
2、注意事项
- 只能在函数组件最外层调用 hooks, 不要在条件、循环、嵌套中调用 hooks
- 不要在其他 JavaScript 函数中调用
3、useState
class 组件中有 state 状态,而函数组件中不存在。使用 useState
就可以为函数组件引入 state。
const [state, setState] = useState(initialState);
- useState 的唯一参数是初始值 initialState。
- useState 返回一个数组,一个是 state,另一个是改变 state 的函数。
- 在初始渲染时,返回的 state 与初始的参数值一样。
- 如果想改变这个state,可以通过调用这个函数来改变,不过这里要注意的是:与 class 组件的 this.setState不一样的是,它不会把新的 state 与旧的 state 进行合并,而是直接替换。
- 调用状态更新函数后,React 重新渲染组件,以使新状态变为当前状态。
3.1 使用例子
interface Child1Props {
num: number;
setNum: Dispatch<SetStateAction<number>>;
}
interface Child2Props {
text: string;
setText: Dispatch<SetStateAction<string>>;
}
const Child1 = (props: Child1Props) => {
const {num, setNum} = props;
return (
<div>
<button onClick={() => setNum(num + 1)}>{num}</button>
</div>
);
};
const Child2 = (props: Child2Props) => {
const {text} = props;
return (
<div>
<span>input: {text}</span>
</div>
);
};
const App = () => {
const [num, setNum] = useState<number>(0);
const [text, setText] = useState<string>('');
return (
<div>
<Child1 num={num} setNum={setNum} />
<Child2 text={text} setText={setText} />
<input type='text' value={text} onChange={e => setText(e.target.value)} />
</div>
);
};
3.2 使用回调更新状态
如果更新的状态需要依赖与前一个state状态,那么可以使用回调来更新状态:
setState(preState => nextState);
例子:
const App = () => {
const [on, setOn] = useState(false);
return (
<>
<span>{on ? '开' : '关'}</span>
<button onClick={() => setOn(on => !on)}>{'on/off'}</button>
</>
);
};
3.3 惰性的初始化
- 初始的 state 只会在初始化渲染时,才会起作用,之后的渲染会被忽略。
- 如果初始的 state 需要通过复杂的计算才能得到,那么可以传入一个函数,该函数只在初次渲染时执行,之后会被忽略。
例子:
someExpensiveComputation
是一个相对耗时的操作,如果我们直接使用:
const initialState = someExpensiveComputation(props);
const [state, setState] = useState(initialState);
这样每次函数被渲染时,都会执行这个 someExpensiveComputation
耗时的操作。 如果使用惰性初始化的方法:
const [state, setState] = useState(()=>someExpensiveComputation(props));
这样只会在初次渲染时执行someExpensiveComputation
这个函数,从而性能优化。
3.4 多个 useState 相互独立
假如一个函数组件有多个状态: 每次 useState 里面都只传一个值,并没有告诉没个值对应的 key 是哪一个,react 如何保证多个 useState 是相互独立的?
答案是: 根据 useState 的出现顺序来决定的。
const TabsBasic = () => {
const [loading, setLoading] = useState<boolean>(false);
const [name, setName] = useState<string>('xiaoqi');
const [num, setNum] = useState<number>(1);
// ...
}
第一次渲染:
useState<boolean>(false); // 将 loading 值设置为 false
useState<string>('xiaoqi'); // 将 name 设置为 xiaoqi
useState<number>(1);// 将 num 设置为 1
第二次渲染:
useState<boolean>(false); // 将 loading 值设置为读取的新值
useState<string>('xiaoqi'); // 同上
useState<number>(1);// 同上
如果将代码修改一下:
const TabsBasic = () => {
let show = true;
const [loading, setLoading] = useState<boolean>(false);
if(show) {
const [name, setName] = useState<string>('xiaoqi');
show = false;
}
const [num, setNum] = useState<number>(1);
// ...
}
再来看一下渲染情况:
第一次渲染:
useState<boolean>(false); // 将 loading 值设置为 false
useState<string>('xiaoqi'); // 将 name 设置为 xiaoqi
useState<number>(1);// 将 num 设置为 1
第二次渲染:
useState<boolean>(false); // 将 loading 值设置为读取的新值
// useState<string>('xiaoqi');
useState<number>(1);// 读取的值是 name 的值,报错
所以前面说的必须把 hooks 放在最外层,不要在条件、循环、嵌套中调用 hooks。
3.5 性能优化
- 与 class 组件不同的是,当传入的值没有发生变化时,组件不会重新渲染。
- 与 class 组件不同的是,传入的值是直接替换,而不是合并,所以要使用扩张运算符。
const App = () => {
const [counter, setCounter] = useState({name: '计数器', number: 0});
return (
<>
<p>
{counter.name} : {counter.number}
</p>
<button
onClick={() => setCounter({...counter, number: counter.number + 1})}>
+
</button>
// 不会重新渲染
<button onClick={() => setCounter({...counter})}>++</button>
</>
);
};
4、useEffect
useEffect 用于处理副作用函数。
- effect 副作用: 比如 ajax 请求、操作原生DOM 元素、绑定/解绑事件、添加订阅、设置定时器、本地持久化缓存等
- useEffect 接受一个函数,该函数会在组件渲染到屏幕之后才执行,该函数要么返回一个清楚副作用的函数,要么就什么不返回。
- useEffect 与 class 组件中的
componentDidMount
、componentDidUpdate
、componentWillUnmount
具有相同的用途 - useEffect 在浏览器渲染完毕之后才会执行,所以不会阻塞视图的更新。
4.1 使用例子
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(()=>{
setTimtout(()=>{
console.log(count)
},3000)
})
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
每次重新渲染组件时,都会执行一次 useEffect 函数。
⚠️注意: useEffect
只接受函数, 所以一下写法是不正确的:
// ❌因为async返回的是个promise对象
useEffect(async() => {
const data = await getAjax()
})
// 建议😄
useEffect(() => {
// 利用匿名函数
(async()=>{ const data = await getAjax() })()
})
4.2 清除副作用
在 useEffect 中返回一个函数,用于清除副作用,这个函数会在组件卸载之前执行。
const Counter = () => {
const [num, setNum] = useState<number>(0);
const [text, setText] = useState<string>('');
useEffect(() => {
let timer = setInterval(() => {
setNum(num => num + 1);
}, 1000);
return () => {
console.log('clear');
clearInterval(timer);
};
});
return (
<>
<input
type='text'
value={text}
onChange={event => setText(event.target.value)}
/>
<p>{num}</p>
<button>+</button>
</>
);
};
在每一次setNum 之后组件都会重新渲染,每次重新渲染之前都会清除定时器。
跳过不必要的副作用
如果一个副作用函数只想再初次渲染时执行,可以给useEffect 传第二个参数。用第二个参数来告诉react 只有当这个依赖值发生变化时,才执行副作用函数。当第二个参数为[]
时,表示只在初次渲染时执行。
// 初次渲染时才执行
useEffect(() => {
document.title = `You clicked ${count} times`;
}, []);
// 只有当count的值发生变化时,才会重新执行
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);