React Hooks初体验,细说Hooks的用法场景

useState Vs setState

useState 对比 setState,最明显的差异就是状态控制颗粒更细。后者是一个 Component 一个 state,state 是一个对象,每次更新 state 会自动合并对象为新的对象。如:

state = {
  name: 'douyu',
  age: 10,
};

// 我需要更新其中一个状态
this.setState({ age: 100 });

不过 useState 也可以使用对象为维度控制,只不过 useState 是覆盖式的更新,同样的代码效果:

const { douyu, setDouyu } = useState({
  name: 'douyu',
  age: 10,
});

// hooks更新其中一个属性
setDouyu({ ...douyu, ...{ age: 100 } });

为何要用 memo(HOC)

看下如下一个简单示例,当父组件因为 state 变化而触发 render,子组件在不做任何处理的时候,也会跟着父组件重新 reder:

import React, { useState } from 'react';

const Img = () => {
  console.log('Img render');

  return <p>img component</p>;
};

export default () => {
  const [count, setCount] = useState(1);
  const addCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p onClick={addCount}>click {count}</p>
      <Img />
    </div>
  );
};

log 如下:

Img render
Img render

memo 的功能类似 PureComponent,可以节约渲染性能:

import React, { useState, memo } from 'react';

const Img = () => {
  console.log('Img render');

  return <p>img component</p>;
};

const ImgMemo = memo(Img);

export default () => {
  const [count, setCount] = useState(1);
  const addCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p onClick={addCount}>click {count}</p>
      <ImgMemo />
    </div>
  );
};

这个时候再点击 click,就会是如下 log:

Img render

对比 react class 组件,memo 其实是函数组件对 class 组件对 shouldComponentUpdate 生命周期的补充。memo 的详细用法:

function MyComponent(props) {
  /* render using props */
}
function areEqual(prevProps, nextProps) {
  /*
  return true if passing nextProps to render would return
  the same result as passing prevProps to render,
  otherwise return false
  */
}
export default React.memo(MyComponent, areEqual);

所以,简单的子组件可以用 memo 包裹来节约计算性能。

为何要用 useCallback

当父组件把 state 通过 props 传给子组件的时候,我们来看下效果是怎么样的:

import React, { useState, memo } from 'react';

const Img = (props) => {
  console.log('Img render');
  return <p>img component: {props.imageData}</p>;
};

const ImgMemo = memo(Img);

export default () => {
  const [count, setCount] = useState(1);
  const [imageData, setImageData] = useState('src1');
  const addCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p onClick={addCount}>click {count}</p>
      <ImgMemo imageData={imageData} />
    </div>
  );
};

看看这里的代码变化:我们给 Img 组件添加了 imageData 属性,imageData 属性是一个字符串类型我们点击 click 试试,发现 Img 没有重新渲染。这里我们再改一下代码:

import React, { useState, memo } from 'react';

const Img = (props) => {
  console.log('Img render');
  return <p>img component: {props.imageData}</p>;
};

const ImgMemo = memo(Img);

export default () => {
  const [count, setCount] = useState(1);
  const [imageData, setImageData] = useState('src1');
  const addCount = () => {
    setCount(count + 1);
  };

  const onImgClick = () => {
    setImageData('src2');
  };

  return (
    <div>
      <p onClick={addCount}>click {count}</p>
      <ImgMemo imageData={imageData} onImgClick={onImgClick} />
    </div>
  );
};

再次点击发现,Img 组件又会重新渲染了,怎么回事呢?因为 Img 的属性有一个非基本类型的属性 onImgClick,这个函数每次父组件渲染的时候都会重新赋值,属性变化了,所以导致子组件重新渲染的解释就合理了。那么这个时候就需要 useCallback  出场了。useCallback 就是保证在父组件初始化的时候创建一个对象,后面都不会再变了,除非 useCallback 的依赖变量发生变化。所以后面的代码应该这样了:

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

const Img = (props) => {
  console.log('Img render');
  return <p>img component: {props.imageData}</p>;
};

const ImgMemo = memo(Img);

export default () => {
  const [count, setCount] = useState(1);
  const [imageData, setImageData] = useState('src1');
  const addCount = () => {
    setCount(count + 1);
  };

  const onImgClick = useCallback(() => {
    setImageData('src2');
  }, []);

  return (
    <div>
      <p onClick={addCount}>click {count}</p>
      <ImgMemo imageData={imageData} onImgClick={onImgClick} />
    </div>
  );
};

这个时候再点击 click,发现子组件不会再重新渲染了。所以,useCallback 是给父组件传递函数作为属性给子组件的时候用的。

为何要用 useMemo

import React, { useState, memo } from 'react';

const Img = (props) => {
  console.log('Img render');
  return <p>img component: {props.imageData.src}</p>;
};

const ImgMemo = memo(Img);

export default () => {
  const [count, setCount] = useState(1);
  const [imageData, setImageData] = useState({
    src: 'src1',
    lastUpdate: Date.now(),
  });
  const addCount = () => {
    setCount(count + 1);
    setImageData({ ...imageData, ...{ lastUpdate: Date.now() } });
  };

  return (
    <div>
      <p onClick={addCount}>click {count}</p>
      <ImgMemo imageData={imageData} />
    </div>
  );
};

我们又把代码做了简单的调整, imageData  由之前的简单数据类型变成了对象数据类型。与此同时,更新 count 的同时也去更新 imageData,但是更新的是 lastUpdate 字段,并没有更新 src 字段。点击 click,Img 子组件也会跟着更新。这里也是浪费计算的,我们需要用到 useMemo。useMemo 可以把对象缓存起来,然后根据依赖的字段变化再更新缓存,这里就刚好可以满足我们的需求:

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

const Img = (props) => {
  console.log('Img render');
  return <p>img component: {props.imageData.src}</p>;
};

const ImgMemo = memo(Img);

export default () => {
  const [count, setCount] = useState(1);
  const [imageData, setImageData] = useState({
    src: 'src1',
    lastUpdate: Date.now(),
  });
  const addCount = () => {
    setCount(count + 1);
    setImageData({ ...imageData, ...{ lastUpdate: Date.now() } });
  };

  const imageMemo = useMemo(() => {
    return imageData;
  }, []);

  const updateImage = () => {
    setImageData({ ...imageData, ...{ lastUpdate: Date.now(), src: 'src2' } });
  };

  return (
    <div>
      <p onClick={addCount}>click {count}</p>
      <p onClick={updateImage}>click2</p>
      <ImgMemo imageData={imageMemo} />
    </div>
  );
};

点击 click 试试,发现 Img 子组件确实没有再更新了,因为 imageData 被 useMemo 缓存起来了。不过这里还没有结束,因为当 imageData 的 src 属性变化了,Img 子组件也没有更新,不信你点击 click2 试试。这样还需要简单改下 useMemo 的用法:

const imageMemo = useMemo(() => {
  return imageData;
}, [imageData.src]);

这个时候,你再点击 click2,发现 Img 子组件可以重新渲染了。点击 click,count 更新,imageData 的 lastUpdate 更新,Img 子组件也不会更新了。所以,useMemo 就是给父组件传递非基本数据类型给子组件的时候用的。

setState 回调

上面我们说到了 useMemo 的用法和场景,考虑如下代码:

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

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

  const [increase, decrease] = useMemo(() => {
    const increase = () => {
      setCount(count + 1);
    };

    const decrease = () => {
      setCount(count - 1);
    };
    return [increase, decrease];
  }, [count]);

  return [count, increase, decrease];
};

const Img = (props) => <p onClick={props.handleClick}>{props.count}</p>;

export default () => {
  const [count, increase] = useCount();

  const handleClick = useCallback(() => {
    console.log('handleClick count=', count);
    increase();
  }, []);

  return <Img handleClick={handleClick} count={count} />;
};

点击 Img 发现数字到 1 就不会再变化,为什么呢?我们分析代码可以发现,handleClick 用了 useCallback,而且没有 deps,所以 handleClick 之后在初始化的时候创建且不会再变,所以它取的 increase 是父组件初始化时候的,这个时候 count 是 0,点击触发一次 count+1,所以 render 了 1,再点击,还是从 0 到 1。你也许知道怎么解了,就是 handleClick 用的 useCallback 的 deps 加上 increase,当然,这是可以的。但是考虑,如果有这么一个场景,handleClick 的需求就是只能创建一次,二次创建会有计算资源浪费。怎么实现呢?这里就可以通过 useMemo 的 useCount 构造器上处理:

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

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

  const [increase, decrease] = useMemo(() => {
    const increase = () => {
      setCount((prevCount) => prevCount + 1);
      // setCount(count + 1);
    };

    const decrease = () => {
      setCount((prevCount) => prevCount - 1);
      // setCount(count - 1);
    };
    return [increase, decrease];
  }, [count]);

  return [count, increase, decrease];
};

const Img = (props) => <p onClick={props.handleClick}>{props.count}</p>;

export default () => {
  const [count, increase] = useCount();

  const handleClick = useCallback(() => {
    console.log('handleClick count=', count);
    increase();
  }, []);

  return <Img handleClick={handleClick} count={count} />;
};

**我们在 setState 的时候,通过回调,可以取到正确的 count 值。****

useEffect

使用 useEffect 完成副作用操作,补齐了 React 的 watch 能力,如果 deps 为空数组,那么就等效 class 组件里的 didComponentMount 生命周期。 useEffect 接收的返回值是函数,等效 class 组件的 componentWillUnmount 生命周期钩子。

useEffect(() => {
  // didComponentMount
  // do sth...
  return () => {
    // componentWillUnmount
    // do sth...
  };
});