前言 react在16版本之后,加入了fiber架构,官方推荐使用纯函数组件,为此react-hook应运而生。
Hook 使用规则 Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React , { useState } from 'react' ;let isName = true ;function Example ( ) { const [count, setCount] = useState (0 ); if (isName){ const [name, setName] = useState ('名字' ); isName = false ; } return ( <div > <p > You clicked {count} times</p > <button onClick ={() => setCount(count + 1)}> Click me </button > </div > ); }
useState useState
接收一个初始值,返回的是一个解构出来的数组,第一个是当前状态(似state),第二个是状态的更新函数(似setState),更新函数与react的setState不同的是,useState的更新函数会将状态替换(replace)而不是合并(merge)。 使用场景:函数组件中需要用到状态值,类似react类组件的state
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React , { useState } from 'react' ;function Example ( ) { const [count, setCount] = useState (0 ); return ( <div > <p > You clicked {count} times</p > <button onClick ={() => setCount(count + 1)}> Click me </button > </div > ); } export default Example ;
默认值:useState()括号后面,可传入值,也可以传函数,该函数只会渲染一遍
1 2 3 const [ count, setCount ] = useState (() => { return props.count || 0 })
更新渲染:当我们在使用 useState 更新值时,组件重新渲染。若传入的更新值时为不变时,组件不会重新渲染。
useReducer 使用场景:由于useState的更新函数采用的是替换的方式,当我们要在函数组件中处理复杂状态时,如对象和数组等等,使用useState就不尽人意了。因此,我们使用useReducer
来解决这一问题
1 const [state, dispatch] = useReducer (reducer, initialArg, init);
它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法,initialArg
为初始值useReducer
本身接受的参数有三个:
reducer:(state, action) => newStat(reducer 与 redux 中的概念其实一样)
initialArg 初始化值
init 是用来进行惰性初始化的:init(initialArg)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import React , { useReducer } from 'react' ;const initialState = {count : 0 , name : '名字' , age : 1 };function reducer (state, action ) { switch (action.type ) { case 'increment' : return {...state, count : state.count + 1 }; case 'decrement' : return {...state, count : state.count - 1 }; default : throw new Error (); } } function Example ( ) { const [state, dispatch] = useReducer (reducer, initialState); return ( <> Count: {state.count} <button onClick ={() => dispatch({type: 'decrement'})}>-</button > <button onClick ={() => dispatch({type: 'increment'})}>+</button > </> ); } export default Example ;
init
是个函数,并以initialArg
为入参,用于操作initialArg
,返回自定义的初始值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import React , { useReducer } from 'react' ;function init (initialCount ) { return {count : initialCount}; } function reducer (state, action ) { switch (action.type ) { case 'increment' : return {count : state.count + 1 }; case 'decrement' : return {count : state.count - 1 }; case 'reset' : return init (action.payload ); default : throw new Error (); } } function Example ({initialCount} ) { const [state, dispatch] = useReducer (reducer, initialCount, init); return ( <> Count: {state.count} <button onClick ={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button > <button onClick ={() => dispatch({type: 'decrement'})}>-</button > <button onClick ={() => dispatch({type: 'increment'})}>+</button > </> ); } export default Example ;
更多关于useReducer用法
useEffect 使用场景:useEffect
副作用,使函数组件拥有了类似react的声明周期。useEffect会在组件每次render之后调用,useEffect
有两个参数,第一个为执行函数,第二个为数组[]
如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import React , { useState, useEffect } from 'react' ;function Example ( ) { const [count, setCount] = useState (0 ); const [dataSources, setDataSources] = useState ([]); useEffect (() => { console .log ("相当于生命周期:componentDidMount+componentDidUpdate" ) }); useEffect (() => { console .log ("相当于生命周期:componentDidMount" ); }, []); useEffect (() => { console .log ("相当于生命周期:componentDidMount" ) console .log ("相当于依赖dataSources状态值的生命周期:componentDidUpdate" ) }, [dataSources]); useEffect (() => { console .log ("相当于生命周期:componentDidMount" ) return () => { console .log ('相当于声明周期:componentWillUnmount' ); } }, []); return ( <div > <p > You clicked {count} times</p > <button onClick ={() => setCount(count + 1)}> Click me </button > </div > ); } export default Example ;
官方提示:与 componentDidMount 或 componentDidUpdate 不同,useEffect是异步的,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。官方建议尽可能使用useEffect,effect 不需要同步地执行 在个别情况下(例如测量布局,页面状态值闪烁bug时),才使用useLayoutEffect代替useEffect, 形成同步,在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制
React.memo和useCallback和useMemo React.memo
使用场景:React.memo
就类似React.PureComponent,专门用于纯函数组件,起作用是对内部对象进行浅比较,判断是否重新渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import React , { useState, useCallback } from 'react' ;function Example ( ) { const [count, setCount] = useState (0 ); const [list, setList] = useState ([]); return ( <div > <p > You clicked {count} times</p > <button onClick ={() => setCount(count + 1)}>addCount</button > <button onClick ={() => setList([...list, 1])}>changeList</button > <Child list ={list} /> </div > ); } const Child = (props ) => { console .log ("进入了组件child" ) return ( <div > 这里是child:list为{props.list.join(',')}</div > ) } export default Example ;
此时,无论是改变count状态值,还是name状态值,Child组件都会重新渲染,如果我们想让Child组件在其props传入的值,即list改变时才渲染。 用React.memo解决,修改Child如下:
1 2 3 4 5 6 const Child = React .memo ((props ) => { console .log ("进入了组件child" ) return ( <div > 这里是child:{props.list.join(',')}</div > ) })
useCallback
使用场景:useCallback
用来缓存方法,类似react组件构造函数里面定义的:this.onChange = this.onChange.bind(this)
,返回一个缓存的函数 上面例子还存在一个问题,如果Child组件传入一个函数作为参数,由于函数为引用类型,使得每次传入Child组件都是一个新的函数实例,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import React , { useState, useCallback } from 'react' ;function Example ( ) { const [count, setCount] = useState (0 ); const [list, setList] = useState ([]); const handleChange = ( ) => { console .log (`selected` ); } return ( <div > <p > You clicked {count} times</p > <button onClick ={() => setCount(count + 1)}>addCount</button > <button onClick ={() => setList([...list, 1])}>changeList</button > <Child list ={list} handleChange ={handleChange} /> </div > ); } const Child = React .memo ((props ) => { console .log ("进入了组件child" ) return ( <div > 这里是child:list为{props.list.join(',')}</div > ) }) export default Example ;
此时,无论是改变count状态值,还是list状态值,Child组件都会重新渲染 用useCallback解决,修改handleChange方法如下:
1 2 3 4 const handleChange = useCallback (() => { console .log (`selected` ); },[])
useMemo
使用场景:useMemo
函数用于缓存需计算操作的状态值,类似Vue的计算属性。第一个参数为计算函数,且必须return返回一个结果,返回一个缓存的值,第二个参数是一个数组[]
,useMemo
执行依赖于次数组的状态值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import React , { useState, useCallback, useMemo } from 'react' ;function Example ( ) { const [count, setCount] = useState (0 ); const [list, setList] = useState ([]); const [a, setA] = useState (0 ) const [b, setB] = useState (0 ) const handleChange = useCallback (() => { console .log (`selected` ); },[]) const memoizedValue = useMemo (() => a + b, [a, b]); return ( <div > <p > You clicked {count} times</p > <button onClick ={() => setCount(count + 1)}>addCount</button > <button onClick ={() => setList([...list, 1])}>changeList</button > <button onClick ={() => { setA(a+1)}}>点我A</button > <button onClick ={() => { setB(b+1)}}>点我B</button > <Child list ={list} memoizedValue ={memoizedValue} handleChange ={handleChange} /> </div > ); } const Child = React .memo ((props ) => { console .log ("进入了组件child" ) return ( <div > 这里是child:list为{props.list.join(',')}, 计算总和:{props.memoizedValue}</div > ) }) export default Example ;
useMemo
和useCallback
相同点: 在依赖数据发生变化的时候,才会调用传进去的回调函数去重新计算结果,起到一个缓存的作用
useMemo
和useCallback
区别
useMemo 缓存的结果是回调函数中return回来的值,主要用于缓存计算结果的值,应用场景如需要计算的状态
useCallback 缓存的结果是函数,主要用于缓存函数,应用场景如需要缓存的函数,因为函数式组件每次任何一个state发生变化,会触发整个组件更新,一些函数是没有必要更新的,此时就应该缓存起来,提高性能,减少对资源的浪费;另外还需要注意的是,useCallback
应该和React.memo
配套使用,缺了一个都可能导致性能不升反而下降。
useRef和React.forwardRef useRef
useRef可以接受一个默认值,并返回一个含有current属性的可变对象; 使用场景: 1、获取子组件的实例(子组件需为react类继承组件); 2、获取组件中某个DOM元素; 3、用做组件的全局变量,useRef返回对象中含有一个current属性,该属性可以在整个组件色生命周期内不变,不会因为重复render而重复申明,类似于react类继承组件的属性this.xxx一样。原因:由于useState保存的变量会触发组件render,而使用useRef定义全局变量不会触发组件render;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import React , { useState, useRef } from 'react' ;function Example ( ) { const [count, setCount] = useState (0 ); const spanEl = useRef (null ); const getSpanRef = ( ) => { console .log (2 , spanEl.current ) }; const sunEl = useRef (null ); const getRefBySun = ( ) => { console .log (1 , sunEl.current ) }; const isClick = useRef (false ); const addCount = ( ) => { if (!isClick.current ) { setCount (count + 1 ) isClick.current = true } }; return ( <> <button onClick ={addCount} > addCount</button > <Sun ref ={sunEl} count ={count} /> <span ref ={spanEl} > 我是span</span > <button onClick ={getSpanRef} > 获取DOM元素Span</button > <button onClick ={getRefBySun} > 获取Sun组件</button > </> ); } class Sun extends React.Component { render () { const { count } = this .props return ( <div > { count }</div > ) } } export default Example ;
React.forwardRef
:使用场景:比较少用,用于在父组件获取子组件的DOM元素作为自己的ref,然后操作子组件的DOM元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import React , { useRef } from 'react' ;function Example ( ) { const inputEl = useRef (null ); const onButtonClick = ( ) => { console .log (inputEl) inputEl.current .focus (); }; return ( <> <TextInputWithFocusButton ref ={inputEl} onButtonClick ={onButtonClick} /> <span > 我是span</span > </> ); } const TextInputWithFocusButton = (props ) => { return ( <> <input ref ={props.ref} type ="text" /> <button onClick ={props.onButtonClick} > Focus the input</button > </> ); } export default Example ;
上面代码报错:Warning: TextInputWithFocusButton: ref
is not a prop.
需使用React.forwardRef包裹注入一个新的ref,修改TextInputWithFocusButton组件如下:
1 2 3 4 5 6 7 8 9 const TextInputWithFocusButton = React .forwardRef ((props, ref ) => { return ( <> <input ref ={ref} type ="text" /> <button onClick ={props.onButtonClick} > Focus the input</button > </> ); })
useContext 使用场景:useContext需要和React.createContext结合起来使用,解决夸组件间的数据传递问题,类似redux。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import React , { useRef, useContext } from 'react' ;const ThemeContext = React .createContext ('white' );const AgeContext = React .createContext ();function Example ( ) { return ( <> <ThemeContext.Provider value ={ 'blue '}> <div > <ChildOfContext /> </div > </ThemeContext.Provider > <span > 我是span</span > </> ); } const ChildOfContext = (props ) => { console .log ("进入了子组件ChildOfContext" ) return ( <div > 这里是子组件ChildOfContext <GrandChildOfContext /> </div > ) } const GrandChildOfContext = (props ) => { console .log ("进入了孙子组件GrandChildOfContext" ) const color = useContext (ThemeContext ); return ( <div > 这里是子组件GrandChildOfContext 颜色是:{color} </div > ) } export default Example ;
使用传统React API方式,修改孙子组件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const GrandChildOfContext = (props ) => { console .log ("进入了孙子组件GrandChildOfContext" ) return ( <ThemeContext.Consumer > {value => ( <div > 这里是子组件GrandChildOfContext 颜色是:{color} </div > )} </ThemeContext.Consumer > ) }
多个context嵌套使用时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import React , { useRef, useContext } from 'react' ;const ThemeContext = React .createContext ('white' );const AgeContext = React .createContext ();function Example ( ) { return ( <> <ThemeContext.Provider value ={ 'blue '}> <div > <ChildOfContext /> </div > </ThemeContext.Provider > <span > 我是span</span > </> ); } const ChildOfContext = (props ) => { console .log ("进入了子组件ChildOfContext" ) return ( <div > 这里是子组件ChildOfContext <AgeContext.Provider value ={21} > <div > <GrandChildOfContext /> </div > </AgeContext.Provider > </div > ) } const GrandChildOfContext = (props ) => { console .log ("进入了孙子组件GrandChildOfContext" ) const color = useContext (ThemeContext ); const age = useContext (AgeContext ); return ( <div > 这里是子组件GrandChildOfContext 颜色是:{color} 嵌套Context 年龄是:{age} </div > ) } export default Example ;
使用传统React API方式,修改孙子组件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const GrandChildOfContext = (props ) => { console .log ("进入了孙子组件GrandChildOfContext" ) return ( <ThemeContext.Consumer > {color => ( <div > 这里是子组件GrandChildOfContext 颜色是:{color} <AgeContext.Consumer > {age => ( <div > 嵌套Context 年龄是:{age} </div > )} </AgeContext.Consumer > </div > )} </ThemeContext.Consumer > ) }
官方提醒:接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。 因此,<MyContext.Provider>包裹的组件越多,嵌套越多,对子组件的影响越大,导致不必要的重渲染太多,所以Context最好就近用,别跨那么多级组件