React hooks 简明教程

929 阅读8分钟

本文参考 React Hooks 解析(上):基础React Hooks 解析(下):进阶React Hooks 入门教程超性感的React Hooks(十一)useCallback、useMemoreact 官网

React v16.8 版本引入的新 api,目的是增强函数组件,使函数组件具备状态管理的能力

State hook (useState)

  • const [state, setState] = useState(initialState); 获取需要的 state更新 state 的方法
    • initialState 当前 state 的值,可以是一个函数,函数的返回值将作为 state 的值,参数只会在组件的初始渲染中起作用
    • 返回值:返回的是一个数组,第一个是当前 state 的值,第二个是更新 state 的方法
/// index.jsx
import React, { useState, useEffect } from 'react';

const Index = () => {
    const [count, setCount] = useState(0); // 
    useEffect(() => {
        console.log(`count: ${count}, age: ${age}, work: ${work}`);
    })
    return (
        <div>
            <p>hooks</p>
            <p>{count}</p>
            <button onClick={() => {setCount(count+1)}}>增加</button>
        </div>
    );
}

export default Index;

Effect Hook

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API

useEffect 会在每次 DOM 渲染后执行,不会阻塞页面渲染。

在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的。

  • useEffect(() => { // Async Action }, [dependencies]) 默认情况下,它在第一次渲染之后和每次更新之后都会执行
    • 第一个参数是一个函数,通常用来编写异步操作的代码。如果这个函数返回了一个函数,那么这个返回的函数,会在组件卸载前执行。
    • 第二个参数是一个数组,用于给出 Effect 的依赖项,只有当数组发生变化时,useEffect() 才会执行。第二个参数如果省略,这时组件只要发生了渲染,就会执行 useEffect()。如果传入的是一个 [] 数组,只会在第一次挂载和卸载是调用 useEffect()
/// index.jsx
import React, { useState, useEffect } from 'react';

const List = () => {
    const [count, setCount] = useState(0);
    useEffect(() => {
        console.log('list 显示'); 
        return () => {
            console.log('list 隐藏');
        } // return 的代码会在卸载前执行
    }, [count]); // 这里控制只有在 count 改变时,执行 useEffect 方法
    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(count+1) }>增加</button>
        </div>
    )
}

const Index = (props) => {
    const [showList, setShowList] = useState(true)
    return (
        <div>
            <p>hooks</p>
            {showList && <List/>}
            <button onClick={() => {setShowList(false)}}>隐藏</button>
            <button onClick={() => {setShowList(true)}}>显示</button>
        </div>
    );
}

export default Index;

useContext

  • const value = useContext(MyContext) 共享状态钩子
    • MyContextReact.createContext 的返回值
      • const context = React.createContext() 返回具有两个值的对象 { Provider, Consumer }
    • 返回值 context 中的内容
/// index.js
import React, {createContext} from 'react';
import ReactDOM from 'react-dom';
import Index from './pages/index'

const appContext = createContext({}); // 创建一个 context 对象

ReactDOM.render(
  <appContext.Provider value={{ // appContext.Provider 提供了一个 Context 对象这个对象可以被子组件共享共享的值由 value 设定
    userName: 'app'
  }}>
  	<Index context={appContext} /> // 由于 useContext 需要指明从哪获取 context值,所以把 context 对象,当作 props 值往子组件中传递
  </appContext.Provider>,
  document.getElementById('root')
);
...

/// index.jsx
import React, { useContext } from 'react';

const Index = (props) => {
    console.log(props);
    const context = useContext(props.context); // useContext 需要传入一个 context 对象,这里是通过组件的属性进行传递
    console.log(context);
    return (
        <div>
            <p>hooks</p>
            <p>{context.userName}</p>
        </div>
    );
}

export default Index;

如果是那种父子组件需要多层传递数据,上面的写法就会显得比较麻烦,可以通过导出 context 对象的方式来优化代码

/// index.js
export const appContext = createContext({}); // 导出 context 对象

ReactDOM.render(
  <appContext.Provider value={{ 
    userName: 'app'
  }}>
  	<Index /> // 删除 props
  </appContext.Provider>,
  document.getElementById('root')
);
...

/// index.jsx
import { appContext } from '../index' // 引入刚刚导出的 context 对象
...
const context = useContext(appContext); // 这样也可以获取 context 对象的值

useReducer action 钩子

  • const [state, dispatch] = useReducer(reducer, initialState);
    • reducer: Reducer 函数
    • initialState: 状态初始值
    • 返回值 返回一个数组,数组的第一额成员是状态的当前值,第二个成员是发送 action 的 dispatch 函数
/// 使用时,可以完全按照 redux 的思路进行管理
/// store/types.js
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
...

/// store/reducers/index.js
import {INCREMENT, DECREMENT} from '../types';

export const defaultState = { // 注意此处和普通 redux 文件的不同,普通 redux,多半会把这个值当成 state 的默认值
    name: 'main',
    count: 0
}

export default (state, action) => { // 这里没有默认值,useReducer 要求默认值通过第二个参数传递
    switch (action.type) {
        case INCREMENT:
            return {
                ...state,
                count: state.count + 1
            }
        case DECREMENT:
            return {
                ...state,
                count: state.count - 1
            }
        default:
            return state;
    }
}
...

/// store/actionCreator.js action的工厂函数
import { INCREMENT, DECREMENT } from './types';

export const increment = () => ({
    type: INCREMENT
})

export const decrement = () => ({
    type: DECREMENT
})
...

/// pages/index.jsx
import React, { useReducer } from 'react';

import reducer, { defaultState } from '../store/reduers/index';
import { increment, decrement } from '../store/actionCreator';

console.log(reducer);

const Index = (props) => {
    const [state, dispatch] = useReducer(reducer, defaultState); // 第一个参数是 reducer 函数,第二个是默认值
    return (
        <div>
            <p>hooks</p>
            <p>{state.count}</p>
            <button onClick={() => {dispatch(increment())}}>增加</button>
            <button onClick={() => {dispatch(decrement())}}>减少</button>
        </div>
    );
}

export default Index;

useLayoutEffect

使用方法和 useEffect 一致,唯一的区别在于执行时机。 useEffect 是非阻塞会在DOM渲染完后执行,useLayoutEffect 会阻塞 DOM 渲染,方法执行完成后,继续 DOM 渲染。

useMemo、useCallback

这两个 hook 都是为了减少不必要的子组件渲染,这类问题进行的两种优化手段

传入的参数一样,得到的结果必定也是一样

import React, {useState, useMemo, useCallback, useEffect, memo} from 'react';

const ShowText = memo(({expensive}) => { // 一个ui展示的组件,memo 的作用就是只有 props 更新时,组件才会重新渲染
    console.log('text');
    const [count, setCount] = useState(expensive());
    useEffect(() => {
        setCount(expensive());
    }, [expensive]);
    return <>
        {count}
    </>

});

const List = () => {
  const [count, setCount] = useState(0); 
  const [value, setValue] = useState(1); // 有两个state值

  function expensive() {
    console.log("computed expensive"); 
    let sum = 0;
    for (let i = 0; i < count * 10; i++) {
      sum += i;
    }
    return sum;
  } // expensive 是依赖 count 计算出的一个“高消耗“的值
  
  console.log('list');
  
  return (
    <div>
      <p>
        {count}-{value}-<ShowText expensive={expensive}/>
      </p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        增加
      </button>
      <input
        type="text"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
    </div>
  );
};

export default List;

按现在的代码运行,只要 count 或 value 更新,都会触发 List 组件的重新渲染,由于父组件的更新,自然就触发了字组件的更新,只是现在的子组件关联的数据其实只有 count(expensive是根据这个值计算而来),也只希望这个值更新时,才需要去触发子组件的更新。

  • useMemo(() => computeExpensiveValue(a, b), [a, b])
    • useMemo缓存计算结果。它接收两个参数,第一个参数为计算过程(回调函数,必须返回一个结果),第二个参数是依赖项(数组),当依赖项中某一个发生变化,结果将会重新计算
    • 这种优化有助于避免在每次渲染时都进行高开销的计算。
   const callBack = useMemo(expensive, [count]); // useMemo是缓存函数的计算结果,如果 count 更新,才会重新去调用 expensive 函数

    return (
        <div>
            <p>
                {count}-{value}-<ShowText expensive={callBack}/>
...
// 由于传入的是具体值,相关使用调整下,这样props不变,使用了 memo 的关系,子组件也不会重新渲染,如果不使用memo,虽然 callBack 不会执行,但子组件还是会重新渲染下
const ShowText = memo(({expensive}) => {
    console.log('text');
    const [count, setCount] = useState(expensive);
    useEffect(() => {
        setCount(expensive);
    }, [expensive]);
    return <>
        {count}
    </>
});
  • useCallback
    • useCallback 的使用几乎与 useMemo 一样,不过 useCallback 缓存的是一个函数体,当依赖项中的一项发现变化,函数体会重新创建
const callBack = useCallback(expensive, [count]); // 和 useMemo 一样的使用方式,这个方法,返回是方法缓存
...
const ShowText = memo(({expensive}) => {
    console.log('text');
    const [count, setCount] = useState(expensive()); // 把传入的值当作函数使用
    useEffect(() => {
        setCount(expensive());
    }, [expensive]);
    return <>
        {count}
    </>
});

useMemo 和 useCallback 都是记忆函数,记忆函数会利用闭包,在确保返回结果一定正确的情况下,减少了重复冗余的计算过程。只不过记忆函数会造成额外的内存消耗,所以在使用时要考虑清楚,这种消耗带来的收益划不划算,否则达不到优化的目的

useRef

用来获取dom元素,或跨渲染周期保存数据

import React, {useState, useRef, useEffect} from 'react';

const List = () => {
    const [count, setCount] = useState(0);
    const [value, setValue] = useState(1);

    const inputElem = useRef(); // 设定一个空的 useRef
    const stayTime = useRef(1); // 内部可以给定一个初始值
    const timeId = useRef();
    
    // useRef上可以存储不影响组件渲染的变量值,而且期间不受组件重绘的影响,比如这里设置了一些条件,那么后续在 10 秒的范围内无论你点击多次下按钮,下面这部分的定时不受影响,所以如果你需要一些在初始化后在组件内一直有效的变量,就可以使用 useRef 进行定义
    useEffect(() => {
      console.log(stayTime.current);
      if (stayTime.current === 1) {
        timeId.current = setInterval(() => {
          stayTime.current = stayTime.current + 1;
          if (stayTime.current > 10) {
            clearInterval(timeId.current);
            console.log("等待10秒");
          }
        }, 1000);
      }
    });

    useEffect(() => {
        console.log(inputElem.current); // 这是普通用法,可以直接获取对应的 DOM 元素
    })

    return (
        <div>
            <p>
                {count}-{value}
            </p>
            <button
                onClick={() => {
                    setCount(count + 1);
                }}
            >
                增加
            </button>
            <input
                type="text"
                value={value}
                ref={inputElem} // 使用定义的 useRef
                onChange={e => setValue(e.target.value)}
            />
        </div>
    );
}

export default List;

useImperativeHandle

  • useImperativeHandle(ref, createHandle, [deps]) 使用 ref 时自定义暴露给父组件的实例值,这个 hook 应当和 React.forwardRef(这个方法可以简单的理解为解决组件件传递 ref 的问题) 配合使用(这样做会减少暴露给父组件的属性)
    • ref 通过 forwardRef 引用父组件的 ref 实例
    • createHandle 回调函数,返回一个对象,对象里存储要暴露给父组件的属性或方法
import React, {useRef, forwardRef, useEffect} from 'react';

const FancyInput = forwardRef((props, ref) => (
    <input ref={ref} type="text" placeholder='输入姓名' />
))

const List = () => {
    const inputElem = useRef();
    useEffect(() => {
        inputElem.current.focus(); // 这里可以通过 ref 的能力,直接获取到组件中的 DOM 元素
    })
    return (
        <div>
            <FancyInput ref={inputElem} />
        </div>
    );
}

export default List;

不过现在有个问题,就是子组件无法指定自己希望暴露出哪些方法或者属性。此时FancyInput 组件并没有进行任何设置,而 List 组件中直接就使用了 DOM 元素的方法和属性。所以当希望控制父组件的调用范围,或者对某些方默认方法进行重写覆盖,就可以使用 useImperativeHandle

import React, {useRef, forwardRef, useEffect, useImperativeHandle} from 'react';

const FancyInput = forwardRef((props, ref) => {
    const inputRef = useRef(); // 组件内建立一个 ref
    useImperativeHandle(ref, () => ({ // 父组件传入的 ref ,这里作为第一个参数使用,第二个参数就是暴露出给父组件使用的方法属性
        focus: () => {
            console.log('子组件中的事件行为');
            inputRef.current.focus()
        }
    }))
    return (
        <input ref={inputRef} type="text" placeholder='输入姓名' />
    )
});

const List = () => {
    const inputElem = useRef();
    useEffect(() => {
        inputElem.current.focus(); // 这样就控制了父组件中的行为,此时只能调用子组件主动暴露出的方法,如果尝试使用其他方法,会直接报错
    })
    return (
        <div>
            <FancyInput ref={inputElem} />
        </div>
    );
}

export default List;

useDebugValue

可用于在 React 开发者工具中显示自定义 hook 的标签,开发调试中使用

自定义hook

普通的函数,只是其中可能引用了其他 hook,完成特定功能

// 比如这,都可以叫做一个自定义 hook
const useCount = count => {
  useEffect(() => {
    console.log("use ", count);
  });
};