React学习笔记 - Hooks详解

217 阅读4分钟

一、useState

useState传参可有两种方式:

  1. 直接传入初始值:React.useState(0) (常用)
  2. 传一个返回初始值的函数:React.useState(()=>0)

setState用法

(1)数据为对象

若使用对象作为state,需注意:

  1. setState时必须传入新对象,否则不会更新
  2. setState不会自动合并未变的属性

【示例】

const [state, setState] = React.useState({a:1, b:2})

只修改属性a:

setState({
    ...state,
    a: 2
})

(2)传函数作实参

setState的参数也可以是函数,优先使用这种方法,可以避免连续 setState 时失败

setState(x=>x+1)
setState(x=>x+1)

二、useReducer

useReducer可看作是useState的复杂版,它可以将对某个数据进行的所有操作封装在一起,简化修改数据时的代码。

首先,创建数据的操作函数reducer

const reducer = (state, action){
	//...
}

然后useReducer(按顺序传入reducer和初始值):

const [state, dispatch] = React.useReducer(reducer, initialValue)

修改数据使用dispatch函数:

dispatch(/* 实参 */)

dispatch会调用reducer,其参数会成为reducer的第二个参数action,旧state为第一个参数,reducer的返回值将成为新的state,并触发更新。

1、案例

useReducer的使用通常按如下四步:

  1. 创建初始值 initialState
  2. 创建所有操作 reducer(state, action){/* ... */}
  3. 调用 useReducer(reducer, initialState),获得读和写的API
  4. 修改数据的传参形式:(type: '操作类型')

【例】封装对n的加、减操作

创建initialStatereducer

const initialState = {
    n:1
}

const reducer(state, action){ //action包含对n的操作类型的信息,如{type:'add', number: 1}
    if(action.type === 'add'){
        return {n: state.n + action.number}
    }else if(action.type === 'minus'){
        return {n: state.n - action.number}
    }else{
        throw new Error('unknown type')
    }
}

调用useReducer

const [n, dispatch] = React.useReducer(reducer, initialState)

修改n

dispatch({type:'add', number:1}) // n + 1
dispatch({type:'minus', number:2}) // n - 2

2、用useReducer代替Redux

通常的实现步骤:

1、将数据集中在一个 store 对象

const store = {/* ... */}

2、将操作集中在 reducer

function reducer(state, action){/* ... */}

3、创建一个 Context

const Context = React.createContext(null)

4、App 组件内创建数据读写API

const [state, dispatch] = useReducer(reducer, store)

5、读写API传给 Context

<Context.Provider value={{state, dispatch}}>
	...
</Context.Provider>

6、子组件获取读写 API

const {state, dispatch} = useContext(Context)

三、useContext

上下文可以实现在一个范围内,将组件的数据共享给后代组件,可视为是局部的全局变量(仅在某个范围内可用)。

【用法步骤】

  1. 创建上下文:const C = createContext(null)
  2. <C.Provider value={/* 共享的数据 */}>圈定作用域
  3. 作用域内用useContext(C)使用上下文

【例】

const C = createContext(null)
function App(props){
    const [n, setN] = useState(1)
    return (
        <C.Provider value={{n, setN}}>
        	<Son />
        </C.Provider>
    )
}

function Son(props){
    const {n, setN} = useContext(C)
    return (
        /* 可以使用n和setN */
    )
}

四、useEffect

React渲染一个组件的大致流程:

App() ----> 得到虚拟DOM ----> 真DOM ----> 渲染引擎渲染页面

useEffect会在渲染引擎渲染完成后执行

五、useLayoutEffect

useLayoutEffect在渲染引擎工作前(上述第三、四步之间)执行。

useLayoutEffect总是比useEffect先执行,为了用户体验,应优先用useEffect(即保证优先渲染)。

六、useMemo

1、memo

考虑以下情景:

App 组件使用了子组件 Son,当父组件更新时,即便组件没变,也会再次调用 Son。

可以用React.memo来消除多余的子组件更新:

const Son = React.memo(/* Son */)

【问题】:

如果给子Son传入了一个函数,Son仍然会重新执行:

function App(props){
    const fn = ()=>{}
    return (
    	<Son onClick={fn} />
    )
}

因为在App每次执行时,fn是不同的(不同的地址),所以会重新执行 Son。

这可以用useMemo解决。

2、useMemo

useMemo可以使对象缓存下来,即每次执行App时,对象不会改变

function App(props){
	const fn = useMemo(()=>{
        return /* 原fn */
    }, [])
}

只有当数组中的数据变化时,才会生成新的fn,否则fn保持不变。

useMemo写法比较复杂,可以用useCallback代替。

3、useCallback

用法:

useCallback(fn, [n]) //等价于:
useMemo(()=>fn, [n])

七、useRef

useRef得到一个对象,这个对象在每次重新执行 App 时是不变的(同一个地址)。

const count = useRef(0)

然后使用count.current读取值。

修改count.current不会触发更新。

1、forwardRef

函数组件不能传递 ref 属性(props不包含ref):

function App(props){
    const ref = useRef()
	return (
        <Son ref={ref} />
    )
}
// error

forwardRef对Son进行封装,就可以接收ref了:

const Son = forwardRef((props, ref)=>{
    // ref参数收到来自App的ref
})

2、useImperativeHandle

实际上就相当于是 setRef,用于自定义 ref 的属性。

【例】

const Son = forwardRef((props, ref)=>{
	useImperaitveHandle(ref, ()=>{
        return /* 返回 ref 的新值 */
    })
})

八、自定义Hook

自定义Hook,将对数据的操作进行封装,然后使用useXXX

九、Stale Closure -- 过时的闭包

React函数组件每次执行都会产生新的变量,这会导致即使重新渲染后,某些闭包函数依然使用了上一次函数组件执行时产生的变量,容易造成误解。