React 进阶系列:Hooks 该怎么用

这是 React 进阶系列的第一篇文章,这个系列内容会包括一些 React 的新知识以及原理内容,有兴趣的能够持续关注。前端

注意:Hooks 在 React 16.8 版本中才正式发布 react

为何要用 Hooks

组件嵌套问题

以前若是咱们须要抽离一些重复的逻辑,就会选择 HOC 或者 render props 的方式。可是经过这样的方式去实现组件,你打开 React DevTools 就会发现组件被各类其余组件包裹在里面。这种方式首先提升了 debug 的难度,而且也很难实现共享状态。git

可是经过 Hooks 的方式去抽离重复逻辑的话,一是不会增长组件的嵌套,二是能够实现状态的共享。github

class 组件的问题

若是咱们须要一个管理状态的组件,那么就必须使用 class 的方式去建立一个组件。可是一旦 class 组件变得复杂,那么四散的代码就很不容易维护。另外 class 组件经过 Babel 编译出来的代码也相比函数组件多得多。数组

Hooks 可以让咱们经过函数组件的方式去管理状态,而且也能将四散的业务逻辑写成一个个 Hooks 便于复用以及维护。闭包

Hooks 怎么用

前面说了一些 Hooks 的好处,接下来咱们就进入正题,经过实现一个计数器来学习几个经常使用的 Hooks。函数

useState

useState 的用法很简单,传入一个初始 state,返回一个 state 以及修改 state 的函数。学习

// useState 返回的 state 是个常量
// 每次组件从新渲染以后,当前 state 和以前的 state 都不相同
// 即便这个 state 是个对象
const [count, setCount] = useState(1)
复制代码

setCount 用法是和 setState 同样的,能够传入一个新的状态或者函数。fetch

setCount(2)
setCount(prevCount => prevCount + 1)
复制代码

useState 的用法是否是很简单。假如如今须要咱们实现一个计数器,按照以前的方式只能经过 class 的方式去写,可是如今咱们能够经过函数组件 + Hooks 的方式去实现这个功能。ui

function Counter() {
  const [count, setCount] = React.useState(0)
  return (
    <div> Count: {count} <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> </div>
  );
}
复制代码

useEffect

如今咱们的计时器需求又升级了,须要在组件更新之后打印出当前的计数,这时候咱们能够经过 useEffect 来实现

function Counter() {
  const [count, setCount] = React.useState(0)
  
  React.useEffect(() => {
    console.log(count)
  })
  
  return (
    <div> Count: {count} <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> </div>
  );
}
复制代码

以上代码当咱们改变计数的时候,就会打印出正确的计数,咱们其实基本能够把 useEffect 当作是 componentDidUpdate,它们的区别咱们能够在下一个例子中看到。

另外 useEffect 还能够返回一个函数,功能相似于 componentWillUnmount

function Counter() {
  const [count, setCount] = React.useState(0)
  
  React.useEffect(() => {
    console.log(count)
    return () => console.log('clean', count)
  })
  
  // ...
}
复制代码

当咱们每次更新计数时,都会先打印 clean 这行 log

如今咱们的需求再次升级了,须要咱们在计数器更新之后延时两秒打印出计数。实现这个再简单不过了,咱们改造下 useEffect 内部的代码便可

React.useEffect(() => {
    setTimeout(() => {
        console.log(count)
    }, 2000)
})
复制代码

当咱们快速点击按钮后,能够在两秒延时之后看到正确的计数。可是若是咱们将这段代码写到 componentDidUpdate 中,事情就变得不同了。

componentDidUpdate() {
    setTimeout(() => {
        console.log(this.state.count)
    }, 2000)
}
复制代码

对于这段代码来讲,若是咱们快速点击按钮,你会在延时两秒后看到打印出了相同的几个计数。这是由于在 useEffect 中咱们经过闭包的方式每次都捕获到了正确的计数。可是在 componentDidUpdate 中,经过 this.state.count 的方式只能拿到最新的状态,由于这是一个对象。

固然若是你只想拿到最新的 state 的话,你可使用 useRef 来实现。

function Counter() {
  const [count, setCount] = React.useState(0)
  const ref = React.useRef(count)
  
  React.useEffect(() => {
    ref.current = count
    setTimeout(() => {
        console.log(ref.current)
    }, 2000)
  })
  
  //...
}
复制代码

useRef 能够用来存储任何会改变的值,解决了在函数组件上不能经过实例去存储数据的问题。另外你还能够 useRef 来访问到改变以前的数据。

function Counter() {
  const [count, setCount] = React.useState(0)
  const ref = React.useRef()
  
  React.useEffect(() => {
    // 能够在从新赋值以前判断先前存储的数据和当前数据的区别
    ref.current = count
  })
  
  <div>
      Count: {count}
      PreCount: {ref.current}
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
  </div>
  
  //...
}
复制代码

如今需求再次升级,咱们须要经过接口来获取初始计数,咱们经过 setTimeout 来模拟这个行为。

function Counter() {
  const [count, setCount] = React.useState();
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    setLoading(true);
    setTimeout(() => {
      setCount(1);
      setLoading(false);
    }, 2000);
  });
  return (
    <div> {!loading ? ( <div> Count: {count} <button onClick={() => setCount(pre => pre + 1)}>+</button> <button onClick={() => setCount(pre => pre - 1)}>-</button> </div> ) : ( <div>loading</div> )} </div>
  );
}
复制代码

若是你去执行这段代码,会发现 useEffect 无限执行。这是由于在 useEffect 内部再次触发了状态更新,所以 useEffect 会再次执行。

解决这个问题咱们能够经过 useEffect 的第二个参数解决

React.useEffect(() => {
    setLoading(true);
    setTimeout(() => {
      setCount(1);
      setLoading(false);
    }, 2000);
}, []);
复制代码

第二个参数传入一个依赖数组,只有依赖的属性变动了,才会再次触发 useEffect 的执行。在上述例子中,咱们传入一个空数组就表明这个 useEffect 只会执行一次。

如今咱们的代码有点丑陋了,能够将请求的这部分代码单独抽离成一个函数,你可能会这样写

const fetch = () => {
    setLoading(true);
    setTimeout(() => {
      setCount(1);
      setLoading(false);
    }, 2000);
}

React.useEffect(() => {
    fetch()
}, [fetch]);
复制代码

可是这段代码出现的问题和一开始的是同样的,仍是会无限执行。这是由于虽然你传入了依赖,可是每次组件更新的时候 fetch 都会从新建立,所以 useEffect 认为依赖已经更新了,因此再次执行回调。

解决这个问题咱们须要使用到一个新的 Hooks useCallback。这个 Hooks 能够生成一个不随着组件更新而再次建立的 callback,接下来咱们经过这个 Hooks 再次改造下代码

const fetch = React.useCallback(() => {
    setLoading(true);
    setTimeout(() => {
      setCount(1);
      setLoading(false);
    }, 2000);
}, [])

React.useEffect(() => {
    fetch()
}, [fetch]);
复制代码

大功告成,咱们已经经过几个 Hooks + 函数组件完美实现了本来须要 class 组件才能完成的事情。

总结

经过几个计数器的需求咱们学习了一些经常使用的 Hooks,接下来总结一下这部分的内容。

  • useState:传入咱们所需的初始状态,返回一个常量状态以及改变状态的函数
  • useEffect:第一个参数接受一个 callback,每次组件更新都会执行这个 callback,而且 callback 能够返回一个函数,该函数会在每次组件销毁前执行。若是 useEffect 内部有依赖外部的属性,而且但愿依赖属性不改变就不重复执行 useEffect 的话,能够传入一个依赖数组做为第二个参数
  • useRef:若是你须要有一个地方来存储变化的数据
  • useCallback:若是你须要一个不会随着组件更新而从新建立的 callback

另外我还封装了几个经常使用的 Hooks API,有兴趣的能够阅读下代码,仓库中的代码会持续更新。

最后

咱们经过这篇文章学习了如何使用 Hooks,若是你还有什么疑问欢迎在评论区与我互动。

我全部的系列文章都会在个人 Github 中最早更新,有兴趣的能够关注下。今年主要会着重写如下三个专栏

  • 重学 JS
  • React 进阶
  • 重写组件

最后,以为内容有帮助能够关注下个人公众号 「前端真好玩」咯,会有不少好东西等着你。

相关文章
相关标签/搜索