前言

在 React 16.8 中,引入了 Hooks 的概念,它旨在更好的复用“状态逻辑”。React Hooks 的核心思想就是“状态+行为”,其中行为指的是“控制状态的逻辑”。

像人们所熟知的那样,模板代码可以通过组件化的形式复用,而在 Hooks 出现前,“状态逻辑”的复用是让人们及其头疼的问题之一,通常情况下,只能有限的复用“逻辑代码”(即只包含行为而没有与之相关的状态),状态则被耦合在了模板中,Hooks 的出现解决了这个问题。

术语解释

  • 属性(Property):不变的数据;
  • 状态(State):变化的数据,一定包含改变状态的“行为”;
  • 行为(Behavior):改变状态的逻辑;
  • 作用(Effect):作用于视图,是行为的一种,但与状态无关,如 DOM 操作、页面跳转;

例子

有一个同步器 Syncer 组件,它将处理传入的 tasks 任务,并在全部任务完成时,将 tasks 清空,然后继续等待下次任务。使用最简单的方式将它实现:

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

function Syncer({ tasks, onSuccess }) {
  useEffect(() => {
    if (tasks.length === 0) {
      return
    }

    for (const task of tasks) {
      // handle tasks...
    }

    // 假设300ms处理完成,仅供演示,这里应该是同步的
    setTimeout(onSuccess, 300)
  }, [tasks])

  return <div>{tasks.length ? 'Syncing...' : 'No tasks'}</div>
}

export default function App() {
  const [tasks, setTasks] = useState([])
  const getTasks = () => [Math.random(), Math.random()]

  return (
    <>
      <button onClick={() => setTasks((tasks) => [...tasks, ...getTasks()])}>Add tasks</button>
      <Syncer tasks={tasks} onSuccess={() => setTasks([])} />
    </>
  )
}

上面的代码运行的很好,但有个问题:tasks 状态被放在了父组件,Syncer 组件就仅仅实现了对“逻辑代码”的复用,状态部分的代码没有得到复用。

改进:使用引用

React 提供了对组件的引用支持,可以通过引用,在外部访问组件内所暴露的方法。这里使用 useImperativeHandle 向外暴露了 addTasks 方法,它被用于向任务队列中添加新的任务。

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

const Syncer = forwardRef((props, ref) => {
  const [tasks, setTasks] = useState([])

  useImperativeHandle(ref, () => ({
    addTasks: (newTasks) => setTasks((tasks) => [...tasks, ...newTasks]),
  }))

  useEffect(() => {
    if (tasks.length === 0) {
      return
    }

    for (const task of tasks) {
      // handle tasks...
    }

    // 假设300ms处理完成,仅供演示,这里应该是同步的
    setTimeout(() => setTasks([]), 300)
  }, [tasks])

  return <div>{tasks.length ? 'Syncing...' : 'No tasks'}</div>
})

export default function App() {
  const refSyncer = useRef()
  const getTasks = () => [Math.random(), Math.random()]

  return (
    <>
      <button onClick={() => refSyncer.current.addTasks(getTasks())}>Add task</button>
      <Syncer ref={refSyncer} />
    </>
  )
}

需要注意的是,代码中的状态 tasks 被移动到了 Syncer 组件内,这实现了对状态的复用,但这种方式不太直观,容易让人产生误解。

改进:使用自定义 Hook

React 鼓励人们封装特定于某类业务的 Hook 函数,当然 GitHub 上也有很多开源的 Hooks。下面代码使用自定义 Hook 的方式实现了相同的逻辑,在代码量减小的同时,易读性得到了提升。

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

function useSyncer(initialTasks) {
  const [tasks, setTasks] = useState(initialTasks)

  useEffect(() => {
    if (tasks.length === 0) {
      return
    }

    for (const task of tasks) {
      // handle tasks...
    }

    // 假设300ms处理完成,仅供演示,这里应该是同步的
    setTimeout(() => setTasks([]), 300)
  }, [tasks])

  return {
    Syncer: () => <div>{tasks.length ? 'Syncing...' : 'No tasks'}</div>,
    addTasks: (newTasks) => setTasks((tasks) => [...tasks, ...newTasks]),
  }
}

export default function App() {
  const { Syncer, addTasks } = useSyncer([])
  const getTasks = () => [Math.random(), Math.random()]

  return (
    <>
      <button onClick={() => addTasks(getTasks())}>Add task</button>
      <Syncer />
    </>
  )
}

总结

再次回想 React Hooks 的核心思想:

  • 关键字:组合、解耦;
  • 与 UI 解耦,UI 不管逻辑是什么,只管渲染;
  • UI 中可以嵌入各种各样的 Hooks(逻辑在 Hooks 中);