Skip to content

React18 Hooks API

1. useState

useState: 定义变量,使其具备类组件的state,让函数式组件拥有更新视图的能力。

基本使用:

ts
const [state, setState] = useState(initData);

Params:

  • initData:默认初始值,有两种情况:函数和非函数,如果是函数,则函数的返回值作为初始值。

Result:

  • state:数据源,用于渲染UI 层的数据源;
  • setState:改变数据源的函数,可以理解为类组件的this.setState

基本使用: 主要介绍两种setState的使用方法。

tsx
import { useState } from 'react';
import { Button, Space } from 'antd';

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <>
      <div>数字:{count}</div>
      <Space>
        <Button type='primary' onClick={() => setCount(count + 1)}>
          第一种方式+1
        </Button>
        <Button type='primary' onClick={() => setCount((v) => v + 1)}>
          第二种方式+1
        </Button>
      </Space>
    </>
  );
};

export default App;

效果:

注意: useState有点类似于PureComponent,它会进行一个比较浅的比较,这就导致了一个问题,如果只是修改对象的某个属性,并不会实时更新。

tsx
import { useState } from 'react';
import { Button } from 'antd';

const App = () => {
  const [obj01, setObj01] = useState({ number: 0 });
  const [obj02, setObj02] = useState({ number: 0 });

  return (
    <>
      <div>设置原对象:{obj01.number}</div>
      <Button
        type='primary'
        onClick={() => {
          obj01.number++;
          setObj01(obj01);
        }}
      >
        点击+1
      </Button>
      <div>设置新对象:{obj02.number}</div>
      <Button
        type='primary'
        onClick={() => {
          setObj02({ number: obj02.number + 1 });
        }}
      >
        点击+1
      </Button>
    </>
  );
};

export default App;

效果:

2. useEffect

useEffect: 副作用,这个钩子成功弥补了函数式组件没有生命周期的缺陷,是我们最常用的钩子之一。

基本使用:

tsx
useEffect(() => {
  return destory;
}, deps);

Params:

  • callback:useEffect的第一个入参,最终返回destory,它会在下一次callback执行之前调用,其作用是清除上次的callback产生的副作用;
  • deps:依赖项,可选参数,是一个数组,可以有多个依赖项,通过依赖去改变,执行上一次的callback返回的destory和新的effect第一个参数callback

模拟挂载和卸载阶段:

事实上,destory会用在组件卸载阶段上,把它当作组件卸载时执行的方法就好,通常用于监听addEventListenerremoveEventListener上,如:

tsx
import { useState, useEffect } from 'react';
import { Button } from 'antd';

const Child = () => {
  useEffect(() => {
    console.log('挂载');

    return () => {
      console.log('卸载');
    };
  }, []);

  return <div>useEffect</div>;
};

const App = () => {
  const [flag, setFlag] = useState(false);

  return (
    <>
      <Button type='primary' onClick={() => setFlag((v) => !v)}>
        {flag ? '卸载' : '挂载'}
      </Button>
      {flag && <Child />}
    </>
  );
};

export default App;

效果:

依赖变化:

dep的个数决定callback什么时候执行,如:

tsx
import { useState, useEffect } from 'react';
import { Button } from 'antd';

const App = () => {
  const [number, setNumber] = useState(0);
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('count改变才会执行');
  }, [count]);

  return (
    <>
      <div>
        number: {number} count: {count}
      </div>
      <Space>
        <Button type='primary' onClick={() => setNumber((v) => v + 1)}>
          number + 1
        </Button>
        <Button type='primary' onClick={() => setCount((v) => v + 1)}>
          count + 1
        </Button>
      </Space>
    </>
  );
};

export default App;

效果:

无限执行:

useEffect的第二个参数 deps 不存在时,会无限执行。更加准确地说,只要数据源发生变化,该函数都会执行,所以请不要这么做,否则会出现不可控的现象。

tsx
import { useState, useEffect } from 'react';
import { Button } from 'antd';

const App = () => {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  useEffect(() => {
    console.log('Frank的文档');
  });

  return (
    <Space>
      <Button type='primary' onClick={() => setCount((v) => v + 1)}>
        数字加一:{count}
      </Button>
      <Button type='primary' onClick={() => setFlag((v) => !v)}>
        状态切换:{JSON.stringify(flag)}
      </Button>
    </Space>
  );
};

export default App;

效果:

3. useContext

useContext: 上下文,类似于Context,其本意就是设置全局共享数据,使所有组件可跨层级实现共享。

useContext的参数一般是由React.createContext创建,通过CountContext.Provider包裹的组件,才能通过useContext获取对应的值。

基本使用:

tsx
const contextValue = useContext(context);

Params:

  • context:一般而言保存的是context对象。

Result:

  • contextValue:返回的数据,也就是context对象内保存的value值。

基本使用: 子组件Child和孙组件Son,共享父组件Index的数据count

tsx
import { useState, createContext, useContext } from 'react';
import { Button } from 'antd';

const CountContext = createContext(-1);

const Child = () => {
  const count = useContext(CountContext);

  return (
    <div style={{ marginTop: 10 }}>
      子组件获取到的count: {count}
      <Son />
    </div>
  );
};

const Son = () => {
  const count = useContext(CountContext);

  return <div style={{ marginTop: 10 }}>孙组件获取到的count: {count}</div>;
};

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <>
      <div>父组件中的count:{count}</div>
      <Button type='primary' onClick={() => setCount((v) => v + 1)}>
        点击+1
      </Button>
      <CountContext.Provider value={count}>
        <Child />
      </CountContext.Provider>
    </>
  );
};

export default App;

效果:

4. useReducer

useReducer: 功能类似于redux,与redux最大的不同点在于它是单个组件的状态管理,组件通讯还是要通过props。简单地说,useReducer相当于是useState的升级版,用来处理复杂的state变化。

基本使用:

tsx
const [state, dispatch] = useReducer(
    (state, action) => {},
    initialArg,
    init
  );

Params:

  • reducer:函数,可以理解为redux中的reducer,最终返回的值就是新的数据源state
  • initialArg:初始默认值;
  • init:可选值,惰性初始化,用于计算初始值的函数。

INFO

问:什么是惰性初始化?

答:惰性初始化是一种延迟创建对象的手段,直到被需要的第一时间才去创建,目的是实现懒加载来提升性能。换句话说,如果有 init,就会取代 initialArg。

Result:

  • state:更新之后的数据源;
  • dispatch:用于派发更新的dispatchAction,可以认为是useState中的setState

基本用法:

tsx
import { useReducer } from 'react';
import { Button } from 'antd';

const App = () => {
  const [count, dispatch] = useReducer((state: number, action: any) => {
    switch (action?.type) {
      case 'add':
        return state + action?.payload;
      case 'sub':
        return state - action?.payload;
      default:
        return state;
    }
  }, 0);

  return (
    <>
      <div>count:{count}</div>
      <Space>
        <Button type='primary' onClick={() => dispatch({ type: 'add', payload: 1 })}>
          加1
        </Button>
        <Button type='primary' onClick={() => dispatch({ type: 'sub', payload: 1 })}>
          减1
        </Button>
      </Space>
    </>
  );
};

export default App;

效果:

特别注意:reducer中,如果返回的state和之前的state值相同,那么组件将不会更新。

比如这个组件是子组件,并不是组件本身,然后对上面的例子稍加更改:

tsx
const Child = ({ count }: { count: number }) => {
  console.log('子组件发生更新');
  return <div>在子组件的count:{count}</div>;
};

const App = () => {
  const [count, dispatch] = useReducer((state: number, action: any) => {
    switch (action?.type) {
      case 'add':
        return state + action?.payload;
      case 'sub':
        return state - action?.payload;
      default:
        return state;
    }
  }, 0);

  return (
    <>
      <div>count:{count}</div>
      <Space>
        // ...
        <Button type='primary' onClick={() => dispatch({ type: 'no', payload: 1 })}>
          无关按钮
        </Button>
      </Space>

      <Child count={count} />
    </>
  );
};

export default App;

效果:

可以看到,当 count 无变化时,子组件并不会更新。

5. useMemo

场景: 在每一次的状态更新中,都会让组件重新绘制,而重新绘制必然会带来不必要的性能开销,为了防止没有意义的性能开销,React Hooks 提供了useMemo函数。

useMemo: 理念与memo相同,都是判断是否满足当前的限定条件来决定是否执行callback函数。它之所以能带来提升,是因为在依赖不变的情况下,会返回相同的引用,避免子组件进行无意义的重复渲染。

基本使用:

tsx
const cacheData = useMemo(fn, deps);

Params:

  • fn:函数,函数的返回值会作为缓存值;
  • deps:依赖项,数组,会通过数组里的值来判断是否进行fn的调用,如果发生了改变,则会得到新的缓存值。

Result:

  • cacheData:更新之后的数据源,即fn函数的返回值,如果deps中的依赖值发生改变,将重新执行fn,否则取上一次的缓存值。

问题源泉:

tsx
import { useState } from 'react';
import { Button } from 'antd';

const usePow = (list: number[]) => {
  return list.map((item: number) => {
    console.log('我是usePow');
    return Math.pow(item, 2);
  });
};

const App = () => {
  const [flag, setFlag] = useState(true);

  const data = usePow([1, 2, 3]);

  return (
    <>
      <div>数字集合:{JSON.stringify(data)}</div>
      <Button type='primary' onClick={() => setFlag((v) => !v)}>
        状态切换{JSON.stringify(flag)}
      </Button>
    </>
  );
};

export default App;

从例子中来看, 按钮切换的flag应该与usePow的数据毫无关系,那么可以猜一猜,当我们切换按钮的时候,usePow中是否会打印:我是 usePow ?

效果:

可以看到,当点击按钮后,会打印我是usePow,这样就会产生开销。毫无疑问,这种开销并不是我们想要见到的结果,所以有了useMemo。 并用它进行如下改造:

tsx
const usePow = (list: number[]) => {
  return useMemo(
    () =>
      list.map((item: number) => {
        console.log(1);
        return Math.pow(item, 2);
      }),
    []
  );
};

效果:

6. useCallback

useCallback:useMemo极其类似,甚至可以说一模一样,唯一不同的点在于,useMemo返回的是值,而useCallback返回的是函数。

基本使用:

tsx
const resfn = useCallback(fn, deps);

Params:

  • fn:函数,函数的返回值会作为缓存值;
  • deps:依赖项,数组,会通过数组里的值来判断是否进行fn的调用,如果依赖项发生改变,则会得到新的缓存值。

Result:

  • resfn:更新之后的数据源,即fn函数,如果deps中的依赖值发生改变,将重新执行fn,否则取上一次的函数。

基本用法:

tsx
import { useState, useCallback, memo } from 'react';
import { Button } from 'antd';

const App = () => {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(true);

  const add = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <>
      <TestButton onClick={() => setCount((v) => v + 1)}>普通点击</TestButton>
      <TestButton onClick={add}>useCallback点击</TestButton>
      <div>数字:{count}</div>
      <Button type='primary' onClick={() => setFlag((v) => !v)}>
        切换{JSON.stringify(flag)}
      </Button>
    </>
  );
};

const TestButton = memo(({ children, onClick = () => {} }: any) => {
  console.log(children);
  return (
    <Button
      type='primary'
      onClick={onClick}
      style={children === 'useCallback点击' ? { marginLeft: 10 } : undefined}
    >
      {children}
    </Button>
  );
});

export default App;

简要说明下,TestButton里是个按钮,分别存放着有无useCallback包裹的函数,在父组件Index中有一个flag变量,这个变量同样与count无关,那么切换按钮的时候,TestButton的执行如下:

可以看到,切换flag的时候,没有经过useCallback的函数会再次执行,而包裹的函数并没有执行(点击“普通点击”按钮的时候,useCallback的依赖项count发生了改变,所以会打印出useCallback点击)。

INFO

问:为什么在 TestButton 中使用了 React.memo,不使用会怎样?

答:useCallback 必须配合 React.memo 进行优化,如果不配合使用,将不会起到性能优化的作用。

7. useRef

useRef: 用于获取当前元素的所有属性,除此之外,还有一个高级用法:缓存数据。

基本使用:

tsx
const ref = useRef(initialValue);

Params:

  • initialValue:初始值,默认值。

Result:

  • ref:返回的一个current对象,这个current属性就是ref对象需要获取的内容。

基本用法:

tsx
import { useState, useRef } from 'react';

const App = () => {
  const scrollRef = useRef<HTMLDivElement>(null);
  const [clientHeight, setClientHeight] = useState<number>(0);
  const [scrollTop, setScrollTop] = useState<number>(0);
  const [scrollHeight, setScrollHeight] = useState<number>(0);

  const onScroll = () => {
    if (scrollRef?.current) {
      const clientHeight = scrollRef?.current.clientHeight; //可视区域高度
      const scrollTop = scrollRef?.current.scrollTop; //滚动条滚动高度
      const scrollHeight = scrollRef?.current.scrollHeight; //滚动内容高度
      setClientHeight(clientHeight);
      setScrollTop(scrollTop);
      setScrollHeight(scrollHeight);
    }
  };

  useEffect(() => onScroll(), []);

  return (
    <>
      <div>
        <p>可视区域高度:{clientHeight}</p>
        <p>滚动条滚动高度:{scrollTop}</p>
        <p>滚动内容高度:{scrollHeight}</p>
      </div>
      <div
        style={{ height: 200, border: '1px solid #000', overflowY: 'auto' }}
        ref={scrollRef}
        onScroll={onScroll}
      >
        <div style={{ height: 2000 }}></div>
      </div>
    </>
  );
};

export default App;

效果:

8. useImperativeHandle

useImperativeHandle: 可以通过refforwardRef暴露给父组件的实例值,所谓的实例值是指值和函数。

实际上这个钩子非常有用,简单来讲,这个钩子可以让不同的模块关联起来,让父组件调用子组件的方法。

举个例子,在一个页面很复杂的时候,我们会将这个页面进行模块化,这样会分成很多个模块,有的时候我们需要在最外层的组件上控制其他组件的方法,希望最外层的点击事件同时执行子组件的事件,这时就需要useImperativeHandle的帮助。

基本使用:

tsx
useImperativeHandle(ref, createHandle, deps);

Params:

  • ref:接受useRefforwardRef传递过来的ref
  • createHandle:处理函数,返回值作为暴露给父组件的ref对象;
  • deps:依赖项,依赖项如果更改,会形成新的ref对象。

父组件是函数式组件:

tsx
import { forwardRef, useState, useRef, useImperativeHandle } from 'react';
import { Button } from 'antd';

const Child = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);

  useImperativeHandle(ref, () => ({ add }));

  const add = () => {
    setCount((v) => v + 1);
  };

  return (
    <div>
      <p>点击次数:{count}</p>
      <Button onClick={() => add()}> 子组件的按钮,点击+1</Button>
    </div>
  );
});

const App = () => {
  const ref = useRef<any>(null);

  return (
    <>
      <Button type='primary' onClick={() => ref.current.add()}>
        父组件上的按钮,点击+1
      </Button>
      <Child ref={ref} />
    </>
  );
};

export default App;

效果:

forwardRef: 引用传递,是一种通过组件向子组件自动传递引用ref的技术。对于应用者的大多数组件来说没什么作用,但对于一些重复使用的组件,可能有用。

9. useLayoutEffect

useLayoutEffect:useEffect基本一致,不同点在于它是同步执行的。简要说明:

  • 执行顺序:useLayoutEffect是在DOM更新之后,浏览器绘制之前的操作,这样可以更加方便地修改DOM,获取DOM信息,这样浏览器只会绘制一次,所以useLayoutEffect的执行顺序在useEffect之前;
  • useLayoutEffect相当于有一层防抖效果;
  • useLayoutEffectcallback中会阻塞浏览器绘制。

基本使用:

tsx
useLayoutEffect(callback, deps);