React Hook填坑

若是已经使用过 Hook,相信你必定回不去了,这种用函数的方式去编写有状态组件简直太爽啦。javascript

若是还没使用过 Hook,那你要赶忙升级你的 React(v16.8+),投入 Hook 的怀抱吧。html

至于 Hook 的好处这里就很少说了,上一篇已经讲过了——React Hook上车java

Hook 虽好,操做不当但是容易翻车的哦。react

下面,咱们就来聊聊在使用过程当中可能遇到的坑吧......git

useState

useState 只在组件首次渲染的时候执行

坑:useState的初始值,只在第一次有效github

证据:npm

当点击按钮修改name的值的时候,我发如今Child组件,是收到了,可是并无经过useState赋值给name!segmentfault

const Child = ({data}) =>{
    console.log('child render...', data) // 每次更新都会执行
    const [name, setName] = useState(data) // 只会在首次渲染组件时执行
    return (
        <div>
            <div>child</div>
            <div>{name} --- {data}</div>
        </div>
    );
}

const Hook =()=>{
    console.log('Hook render...')
    const [name, setName] = useState('rose')
    return(
        <div>
            <div>
                {count}
            </div>
            <button onClick={()=>setName('jack')}>update name </button>
            <Child data={name}/>
        </div>
    )
}

想在第一次 render 前执行的代码放 useState() 里面

上面咱们已经知道了useState()只会在第一次渲染的时候才执行,那么这有什么实用价值吗?答案:能够把第一次 render 前执行的代码放入其中。数组

例如:缓存

const instance = useRef(null);
useState(() => {
  instance.current = 'initial value';
});

相似 class component 里的constructorcomponentWillMount

useState 里数据必须为 immutable

啥?你还不知道 immutable 是个啥?甩手就是两个连接:Immutable.js 了解一下Immutable 详解及在 React 实践

什么是 Immutable Data?
首先,你要知道 JavaScript 中的对象通常是可变的(Mutable Data),由于使用了引用赋值。
Immutable Data 就是一旦建立,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操做都会返回一个新的 Immutable 对象。

虽然 class component 的 state 也提倡使用 immutable data,但不是强制的,由于只要调用了setState就会触发更新。

可是使用useState时,若是在更新函数里传入同一个对象将没法触发更新

证据:

const [list, setList] = useState([2,32,1,534,44]);
return (
  <>
    <ol>
      {list.map(v => <li key={v}>{v}</li>)}
    </ol>
    <button
      onClick={() => {
        // bad:这样没法触发更新!
        setList(list.sort((a, b) => a - b));
        // good:必须传入一个新的对象!
        setList(list.slice().sort((a, b) => a - b));
      }}
    >sort</button>
  </>
)

useState 过期的闭包

以前就说过,Hook 产生问题时,90%都是闭包引发的。下面就来看一下这个诡异的bug:

function DelayCount() {
  const [count, setCount] = useState(0);

  function handleClickAsync() {
    setTimeout(function delay() {
      setCount(count + 1); // 问题所在:此时的 count 为1s前的count!!!
    }, 1000);
  }

  function handleClickSync() {
    setCount(count + 1);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>异步加1</button>
      <button onClick={handleClickSync}>同步加1</button>
    </div>
  );
}

点击“异步加1”按键,而后当即点击“同步加1”按钮。你会惊奇的发现,count 只更新到 1。

这是由于 delay() 是一个过期的闭包。

来看看这个过程发生了什么:

  1. 初始渲染:count 值为 0。
  2. 点击“异步加1”按钮,delay() 闭包捕获 count 的值 0,setTimeout() 1 秒后调用 delay()。
  3. 点击“同步加1”按钮,handleClickSync() 调用 setCount(0 + 1) 将 count 的值设置为 1,组件从新渲染。
  4. 1 秒以后,setTimeout() 执行 delay() 。可是 delay() 中闭包保存 count 的值是初始渲染的值 0,因此调用 setState(0 + 1),结果count保持为 1。

delay() 是一个过期的闭包,它使用在初始渲染期间捕获的过期的 count 变量。

为了解决这个问题,可使用函数方法来更新 count 状态:

function DelayCount() {
  const [count, setCount] = useState(0);

  function handleClickAsync() {
    setTimeout(function delay() {
      setCount(count => count + 1); // 重点:setCount传入的回调函数用的是最新的 state!!!
    }, 1000);
  }

  function handleClickSync() {
    setCount(count + 1);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>异步加1</button>
      <button onClick={handleClickSync}>同步加1</button>
    </div>
  );
}

useEffect

如何在 useEffect 中使用 async

上一篇文章中咱们提到过:useEffect的 callback 函数要么返回一个能清除反作用的函数,要么就不返回任何内容。

而 async 函数返回的是 Promise 对象,那咱们要怎么在 useEffect 的callback 中使用 async 呢?

最简单的方法是IIFE(自执行函数):

useEffect(() => {
  (async () => {
    await fetchSomething();
  })();
}, []);

useEffect 死循环

  1. useEffect 在传入第二个参数时必定注意:第二个参数不能为引用类型,会形成死循环。
    好比:[]===[] 为false,因此会形成 useEffect 会一直不停的渲染。
  2. useEffect 的 callback 函数中改变的 state 必定不能在该 useEffect 的依赖数组中。好比:useEffect(()=>{ setCount(count); }, [count]);依赖 count,callback 中又 setCount(count)。

推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。

函数做为依赖的时候死循环

有时候,咱们须要将函数做为依赖项传入依赖数组中,例如:

// 子组件
let Child = React.memo((props) => {
  useEffect(() => {
    props.onChange(props.id)
  }, [props.onChange, props.id]);
  
  return (
    <div>{props.id}</div>
  );
});

// 父组件
let Parent = () => {
  let [id, setId] = useState(0);
  let [count, setCount] = useState(0);
  const onChange = (id) => {
    // coding
    setCount(id);
  }
  
  return (
    <div>
    	{count}
      <Child onChange={onChange} id={id} />  // 重点:这里有性能问题!!!
    </div>
  );
};

代码中重点位置,每次父组件render,onChange引用值确定会变。所以,子组件Child一定会render,子组件触发useEffect,从而再次触发父组件render....循环往复,这就会形成死循环。下面咱们来优化一下:

// 子组件
let Child = React.memo((props) => {
  useEffect(() => {
    props.onChange(props.id)
  }, [props.onChange, props.id]);
  return (
    <div>{props.id}</div>
  );
});

// 父组件
let Parent = () => {
  let [id, setId] = useState(0);
  let [count, setCount] = useState(0);
  const onChange = useCallback(() => { // 重点:经过useCallback包裹一层便可达到缓存函数的目的
    // coding
  }, [id]); // id 为依赖值
  return (
    <div>
    	{count}
      <Child onChange={onChange} id={id} />  // 重点:这个onChange在每次父组件render都会改变!
    </div>
  );
};

useCallback将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给通过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将很是有用。

useCallback(fn, deps) 至关于 useMemo(() => fn, deps)

useEffect 里面拿不到最新的props和state

useEffect里面使用到的 state 的值, 固定在了useEffect内部,不会被改变,除非useEffect刷新,从新固定 state 的值。

useRef保存任何可变化的值,.current属性老是取最新的值。

function Example() {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);

  useEffect(() => {
    // Set the mutable latest value
    latestCount.current = count;
    
    setTimeout(() => {
      // Read the mutable latest value
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });

总结

以上只是收集了一部分工做中可能会遇到的坑,大体分为2种:

  1. 闭包引发的 state 值过时
  2. 依赖值监听问题致使死循环

之后遇到其余的问题会继续补充...

参考:
react hooks踩坑记录

使用 JS 及 React Hook 时须要注意过期闭包的坑(文中有解决方法)

终于搞懂react hooks了!!!!

相关文章
相关标签/搜索