在 React 16.8 中,引入了 Hooks 的概念,它旨在更好的复用“状态逻辑”。React Hooks 的核心思想就是“状态+行为”,其中行为指的是“控制状态的逻辑”。
像人们所熟知的那样,模板代码可以通过组件化的形式复用,而在 Hooks 出现前,“状态逻辑”的复用是让人们及其头疼的问题之一,通常情况下,只能有限的复用“逻辑代码”(即只包含行为而没有与之相关的状态),状态则被耦合在了模板中,Hooks 的出现解决了这个问题。
有一个同步器 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
组件内,这实现了对状态的复用,但这种方式不太直观,容易让人产生误解。
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 的核心思想: