React Hooks
React 在 v16.8 的版本中推出了 React Hooks 新特性。使用 React Hooks 相比于从前的类组件有以下几点好处:
- 代码可读性更强,原本同一块功能的代码逻辑被拆分在了不同的生命周期函数中,容易使开发者不利于维护和迭代,通过 React Hooks 可以将功能代码聚合,方便阅读维护;
- 组件树层级变浅,在原本的代码中,经常使用 HOC/render props 等方式来复用组件的状态,增强功能等,无疑增加了组件树层数及渲染,而在 React Hooks 中,这些功能都可以通过强大的自定义的 Hooks 来实现。
一、State Hook
1、基础用法
function State(){const [count, setCount] = useState(0);return (<div><p>You clicked {count} times</p><button onClick={() => setCount(count + 1)}>Click me</button></div>)}
2、更新
更新分为以下两种方式,即直接更新和函数式更新,其应用场景的区分点在于:
直接更新不依赖于旧 state 的值;函数式更新依赖于旧 state 的值;// 直接更新setState(newCount);// 函数式更新setState(prevCount => prevCount - 1);
3、实现合并
与 class 组件中的setState方法不同,useState 不会自动合并更新对象,而是直接替换它。可以用函数式的setState结合展开运算符来达到合并更新对象的效果。setState(prevState => {// 也可以使用 Object.assignreturn {...prevState, ...updatedValues};});
4、惰性初始化 state
initialState参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。其应用场景在于:创建初始 state 很昂贵时,例如需要通过复杂计算获得;那么则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:const [state, setState] = useState(() => {const initialState = someExpensiveComputation(props);return initialState;});
5、一些重点
(1)不像 class 中的this.setState,Hook 更新 state 变量总是替换它而不是合并它;
(2)推荐使用多个 state 变量,而不是单个 state 变量,因为 state 的替换逻辑而不是合并逻辑,并且利于后续的相关 state 逻辑抽离;
(3)调用 State Hook 的更新函数并传入当前的state时,React 将跳过子组件的渲染及 effect 的执行。(React 使用Object.is比较算法 来比较state。)二、Effect Hook
1、基础用法
function Effect(){const [count, setCount] = useState(0);useEffect(() => {console.log(`You clicked ${count} times`);});return (<div><p>You clicked {count} times</p><button onClick={() => setCount(count + 1)}>Click me</button></div>)}
2、清除操作
为防止内存泄漏,清除函数会在组件卸载前执行;如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除,即先执行上一个 effect 中 return 的函数,然后再执行本 effect 中非 return 的函数。useEffect(() => {const subscription = props.source.subscribe();return () => {// 清除订阅subscription.unsubscribe();};});
3、执行时期
与componentDidMount或componentDidUpdate不同,使用useEffect调度的 effect 不会阻塞浏览器更新屏幕,这让应用看起来响应更快;(componentDidMount或componentDidUpdate会阻塞浏览器更新屏幕)4、性能优化
默认情况下,React 会每次等待浏览器完成画面渲染之后延迟调用 effect;但是如果某些特定值在两次重渲染之间没有发生变化,可以通知 React 跳过对 effect 的调用,只要传递数组作为useEffect的第二个可选参数即可:如下所示,如果 count 值两次渲染之间没有发生变化,那么第二次渲染后就会跳过 effect 的调用;useEffect(() => {document.title = `You clicked ${count} times`;}, [count]); // 仅在 count 更改时更新
5、模拟
如果想只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([ ])作为第二个参数,如下所示,原理跟第 4 点性能优化讲述的一样;componentDidMountuseEffect(() => {.....}, []);
6、最佳实践
要记住 effect 外部的函数使用了哪些 props 和 state 很难,这也是为什么通常会想要在 effect 内部 去声明它所需要的函数。
如果处于某些原因无法把一个函数移动到 effect 内部,还有一些其他办法:// bad,不推荐function Example({ someProp }) {function doSomething() {console.log(someProp);}useEffect(() => {doSomething();}, []); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)}// good,推荐function Example({ someProp }) {useEffect(() => {function doSomething() {console.log(someProp);}doSomething();}, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)}
可以尝试把那个函数移动到组件之外。那样一来,这个函数就肯定不会依赖任何 props 或 state,并且也不用出现在依赖列表中了;万不得已的情况下,可以把函数加入 effect 的依赖但 把它的定义包裹进useCallbackHook。这就确保了它不随渲染而改变,除非它自身的依赖发生了改变;
推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则,此规则会在添加错误依赖时发出警告并给出修复建议 ;// 1、安装插件
npm i eslint-plugin-react-hooks --save-dev
// 2、eslint 配置
{"plugins": [// ..."react-hooks"],"rules": {// ..."react-hooks/rules-of-hooks": "error","react-hooks/exhaustive-deps": "warn"}}
7、一些重点
(1)可以把 useEffect Hook 看做componentDidMount,componentDidUpdate和componentWillUnmount这三个函数的组合;
(2)在 React 的 class 组件中,render 函数是不应该有任何副作用的;一般来说,在这里执行操作太早了,基本上都希望在 React 更新 DOM 之后才执行操作。三、
用来处理多层级传递数据的方式,在以前组件树中,跨层级祖先组件想要给孙子组件传递数据的时候,除了一层层 props 往下透传之外,还可以使用 React Context API 来做这件事。使用例子如下所示useContext
(1)使用 React Context API,在组件外部建立一个 Context
(2)使用import React from 'react';const ThemeContext = React.createContext(0);export default ThemeContext;
Context.Provider提供了一个 Context 对象,这个对象可以被子组件共享
(3)import React, { useState } from 'react';import ThemeContext from './ThemeContext';import ContextComponent1 from './ContextComponent1';function ContextPage () {const [count, setCount] = useState(1);return (<div className="App"><ThemeContext.Provider value={count}><ContextComponent1 /></ThemeContext.Provider><button onClick={() => setCount(count + 1)}>Click me</button></div>);}export default ContextPage;
useContext()钩子函数用来引入 Context 对象,并且获取到它的值 // 子组件,在子组件中使用孙组件
// 孙组件,在孙组件中使用 Context 对象值import React from 'react';import ContextComponent2 from './ContextComponent2';function ContextComponent () {return (<ContextComponent2 />);}export default ContextComponent;
import React, { useContext } from 'react';import ThemeContext from './ThemeContext';function ContextComponent () {const value = useContext(ThemeContext);return (<div>useContext:{value}</div>);}export default ContextComponent;
四、
useReducer1、基础用法
比useState更适用的场景:例如 state 逻辑处理较复杂且包含多个子值,或者下一个state依赖于之前的state等;例子如下所示import React, { useReducer } from 'react';interface stateType {count: number}interface actionType {type: string}const initialState = { count: 0 };const reducer = (state:stateType, action:actionType) => {switch (action.type) {case 'increment':return { count: state.count + 1 };case 'decrement':return { count: state.count - 1 };default:throw new Error();}};const UseReducer = () => {const [state, dispatch] = useReducer(reducer, initialState);return (<div className="App"><div>useReducer Count:{state.count}</div><button onClick={() => { dispatch({ type: 'decrement' }); }}>useReducer 减少</button><button onClick={() => { dispatch({ type: 'increment' }); }}>useReducer 增加</button></div>);};export default UseReducer;
2、惰性初始化
stateinterface stateType {count: number}interface actionType {type: string,paylod?: number}const initCount =0const init = (initCount:number)=>{return {count:initCount}}const reducer = (state:stateType, action:actionType)=>{switch(action.type){case 'increment':return {count: state.count + 1}case 'decrement':return {count: state.count - 1}case 'reset':return init(action.paylod || 0)default:throw new Error();}}const UseReducer = () => {const [state, dispatch] = useReducer(reducer,initCount,init)return (<div className="App"><div>useReducer Count:{state.count}</div><button onClick={()=>{dispatch({type:'decrement'})}}>useReducer 减少</button><button onClick={()=>{dispatch({type:'increment'})}}>useReducer 增加</button><button onClick={()=>{dispatch({type:'reset',paylod:10 })}}>useReducer 增加</button></div>);}export default UseReducer;
五、Memo
如下所示,当父组件重新渲染时,子组件也会重新渲染,即使子组件的 props 和 state 都没有改变
改进:可以使用 memo 包一层,就能解决上面的问题;但是仅仅解决父组件没有传参给子组件的情况以及父组件传简单类型的参数给子组件的情况(例如 string、number、boolean等);如果有传复杂属性应该使用import React, { memo, useState } from 'react';// 子组件const ChildComp = () => {console.log('ChildComp...');return (<div>ChildComp...</div>);};// 父组件const Parent = () => {const [count, setCount] = useState(0);return (<div className="App"><div>hello world {count}</div><div onClick={() => { setCount(count => count + 1); }}>点击增加</div><ChildComp/></div>);};export default Parent;
useCallback(回调事件)或者useMemo(复杂属性)// 子组件const ChildComp = () => {console.log('ChildComp...');return (<div>ChildComp...</div>);};const MemoChildComp = memo(ChildComp);
六、
假设以下场景,父组件在调用子组件时传递 info 对象属性,点击父组件按钮时,发现控制台会打印出子组件被渲染的信息。useMemoimport React, { memo, useState } from 'react';// 子组件const ChildComp = (info:{info:{name: string, age: number}}) => {console.log('ChildComp...');return (<div>ChildComp...</div>);};const MemoChildComp = memo(ChildComp);// 父组件const Parent = () => {const [count, setCount] = useState(0);const [name] = useState('jack');const [age] = useState(11);const info = { name, age };return (<div className="App"><div>hello world {count}</div><div onClick={() => { setCount(count => count + 1); }}>点击增加</div><MemoChildComp info={info}/></div>);};export default Parent;
分析原因:
点击父组件按钮,触发父组件重新渲染;父组件渲染,const info = { name, age }一行会重新生成一个新对象,导致传递给子组件的 info 属性值变化,进而导致子组件重新渲染。解决:
使用
useMemo将对象属性包一层,useMemo有两个参数:
- 第一个参数是个函数,返回的对象指向同一个引用,不会创建新对象;
第二个参数是个数组,只有数组中的变量改变时,第一个参数的函数才会返回一个新的对象。
import React, { memo, useMemo, useState } from 'react';// 子组件const ChildComp = (info:{info:{name: string, age: number}}) => {console.log('ChildComp...');return (<div>ChildComp...</div>);};const MemoChildComp = memo(ChildComp);// 父组件const Parent = () => {const [count, setCount] = useState(0);const [name] = useState('jack');const [age] = useState(11);// 使用 useMemo 将对象属性包一层const info = useMemo(() => ({ name, age }), [name, age]);return (<div className="App"><div>hello world {count}</div><div onClick={() => { setCount(count => count + 1); }}>点击增加</div><MemoChildComp info={info}/></div>);};export default Parent;
七 、
useCallback接着第六章节的例子,假设需要将事件传给子组件,如下所示,当点击父组件按钮时,发现控制台会打印出子组件被渲染的信息,说明子组件又被重新渲染了。
import React, { memo, useMemo, useState } from 'react';// 子组件const ChildComp = (props:any) => {console.log('ChildComp...');return (<div>ChildComp...</div>);};const MemoChildComp = memo(ChildComp);// 父组件const Parent = () => {const [count, setCount] = useState(0);const [name] = useState('jack');const [age] = useState(11);const info = useMemo(() => ({ name, age }), [name, age]);const changeName = () => {console.log('输出名称...');};return (<div className="App"><div>hello world {count}</div><div onClick={() => { setCount(count => count + 1); }}>点击增加</div><MemoChildComp info={info} changeName={changeName}/></div>);};export default Parent;
分析下原因:
点击父组件按钮,改变了父组件中 count 变量值(父组件的 state 值),进而导致父组件重新渲染;父组件重新渲染时,会重新创建 changeName 函数,即传给子组件的 changeName 属性发生了变化,导致子组件渲染;
解决:
修改父组件的 changeName 方法,用
useCallback钩子函数包裹一层,useCallback参数与useMemo类似import React, { memo, useCallback, useMemo, useState } from 'react';// 子组件const ChildComp = (props:any) => {console.log('ChildComp...');return (<div>ChildComp...</div>);};const MemoChildComp = memo(ChildComp);// 父组件const Parent = () => {const [count, setCount] = useState(0);const [name] = useState('jack');const [age] = useState(11);const info = useMemo(() => ({ name, age }), [name, age]);const changeName = useCallback(() => {console.log('输出名称...');}, []);return (<div className="App"><div>hello world {count}</div><div onClick={() => { setCount(count => count + 1); }}>点击增加</div><MemoChildComp info={info} changeName={changeName}/></div>);};export default Parent;
八、
useRef1、指向 dom 元素
如下所示,使用 useRef 创建的变量指向一个 input 元素,并在页面渲染后使 input 聚焦
import React, { useRef, useEffect } from 'react';const Page1 = () => {const myRef = useRef<HTMLInputElement>(null);useEffect(() => {myRef?.current?.focus();});return (<div><span>UseRef:</span><input ref={myRef} type="text"/></div>);};export default Page1;
2、存放变量
useRef 在 react hook 中的作用, 正如官网说的, 它像一个变量, 类似于 this , 它就像一个盒子, 可以存放任何东西. createRef 每次渲染都会返回一个新的引用,而
useRef每次都会返回相同的引用,如下例子所示:import React, { useRef, useEffect, useState } from 'react'; const Page1 = () => { const myRef2 = useRef(0); const [count, setCount] = useState(0) useEffect(()=>{ myRef2.current = count; }); function handleClick(){ setTimeout(()=>{ console.log(count); // 3 console.log(myRef2.current); // 6 },3000) } return ( <div> <div onClick={()=> setCount(count+1)}>点击count</div> <div onClick={()=> handleClick()}>查看</div> </div> ); } export default Page1;九、
useImperativeHandle使用场景:通过 ref 获取到的是整个 dom 节点,通过
useImperativeHandle可以控制只暴露一部分方法和属性,而不是整个 dom 节点。十、
useLayoutEffect其函数签名与
useEffect相同,但它会在所有的 DOM 变更之后同步调用effect,这里不再举例。useLayoutEffect和平常写的 Class 组件的componentDidMount和componentDidUpdate同时执行;useEffect会在本次更新完成后,也就是第 1 点的方法执行完成后,再开启一次任务调度,在下次任务调度中执行useEffect。
