Skip to main content

自定义 Hook 创建与使用

一、什么是自定义 Hook

除了 React 提供的几种内置 Hook,还可以自定义 Hook,将组件逻辑提取到可重用的函数中。自定义 Hook 约定成俗的使用 use 开头来命名,方便使用 ESLint 来进行检查。

二、自定义 Hook 实践

1、计数器逻辑提取

实现了一个计数器应用,它的值可以递增、递减或重置。代码如下:

import React, { useState } from 'react'

const App = () => {
const [counter, setCounter] = useState(0)

return (
<div>
<div>{counter}</div>
<button onClick={() => setCounter(counter + 1)}>plus</button>
<button onClick={() => setCounter(counter - 1)}>minus</button>
<button onClick={() => setCounter(0)}>zero</button>
</div>
)
}

export default App

效果如下:

现在将计数器逻辑提取到自定义 Hook 中,Hook 的代码如下:

useCounter.js
import { useState } from 'react'

const useCounter = () => {
const [value, setValue] = useState(0)

const increase = () => {
setValue(value + 1)
}

const decrease = () => {
setValue(value - 1)
}

const zero = () => {
setValue(0)
}

return {
value,
increase,
decrease,
zero
}
}

export default useCounter

上面的自定义 Hook 在内部使用 useState 来创建自己的状态。Hook 返回一个对象,其属性包括计数器的值以及操作值的函数。下面在组件中使用该自定义 Hook

import React, { useState } from 'react'
import React from 'react'
import useCounter from './useCounter'

const App = () => {
const [counter, setCounter] = useState(0)
const counter = useCounter()

return (
<div>
<div>{counter}</div>
<button onClick={() => setCounter(counter + 1)}>plus</button>
<button onClick={() => setCounter(counter - 1)}>minus</button>
<button onClick={() => setCounter(0)}>zero</button>
<div>{counter.value}</div>
<button onClick={counter.increase}>plus</button>
<button onClick={counter.decrease}>minus</button>
<button onClick={counter.zero}>zero</button>
</div>
)
}

export default App

上面将 App 组件的状态及其操作完全提取到 useCounter Hook 中,管理计数器状态和逻辑现在就成了自定义 Hook 的责任。

2、表单通用逻辑复用

React 中处理表单有点棘手,举个例子:

下面是个表单,要求用户输入姓名和出生日期:

import React, { useState } from 'react'

const App = () => {
const [name, setName] = useState('')
const [born, setBorn] = useState('')

return (
<>
<form>
<div>
<label>name:</label>
<input
type="text"
value={name}
onChange={(event) => setName(event.target.value)}
/>
</div>
<div>
<label>birthdate:</label>
<input
type="date"
value={born}
onChange={(event) => setBorn(event.target.value)}
/>
</div>
</form>
<div>你的名字是 {name || '__'}</div>
<div>出生日期是 {born || '__'}</div>
</>
)
}

export default App

效果如下:

其中每个字段都有自己的状态,为了使表单的状态与用户提供的数据保持同步,需要给每个 input 元素注册对应的 onChange 处理函数。

下面用自定义的 useField hook 来简化了表单的状态管理:

useField.js
import { useState } from 'react'

const useField = (type) => {
const [value, setValue] = useState('')

const onChange = (event) => {
setValue(event.target.value)
}

return {
type,
value,
onChange
}
}

export default useField

Hook 函数接收 input 字段的类型属性作为参数,返回 input 所需的所有属性:类型、值和 onChange 处理函数。使用如下:

import React, { useState } from 'react'
import React from 'react'
import useField from './useField'

const App = () => {
const [name, setName] = useState('')
const name = useField('text')
const [born, setBorn] = useState('')
const born = useField('date')

return (
<>
<form>
<div>
<label>name:</label>
<input
type="text"
type={name.type}
value={name}
value={name.value}
onChange={(event) => setName(event.target.value)}
onChange={name.onChange}
/>
</div>
<div>
<label>birthdate:</label>
<input
type="date"
type={born.type}
value={born}
value={born.value}
onChange={(event) => setBorn(event.target.value)}
onChange={born.onChange}
/>
</div>
</form>
<div>你的名字是 {name || '__'}</div>
<div>你的名字是 {name.value || '__'}</div>
<div>出生日期是 {born || '__'}</div>
<div>出生日期是 {born.value || '__'}</div>
</>
)
}

export default App

3、展开属性优化写法

上面自定义 useField hook 的使用可以进一步简化。因为 name 对象具有 input 元素期望作为 props 接收的所有属性,所以可以使用展开语法props 传递给元素:

import React from 'react'
import useField from './useField'

const App = () => {
const name = useField('text')
const born = useField('date')

return (
<>
<form>
<div>
<label>name:</label>
<input type={name.type} value={name.value} onChange={name.onChange} />
<input {...name} />
</div>
<div>
<label>birthdate:</label>
<input type={born.type} value={born.value} onChange={born.onChange} />
<input {...born} />
</div>
</form>
<div>你的名字是 {name.value || '__'}</div>
<div>出生日期是 {born.value || '__'}</div>
</>
)
}

export default App

4、获取元素 resize 后的宽高

useWindowSize.js
import { useEffect, useState } from 'react'

export default function useWindowSize(el) {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined
})
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
})
}
window.addEventListener('resize', handleResize)
handleResize()
return () => window.removeEventListener('resize', handleResize)
}, [el])
return windowSize
}

点击查看更多自定义 Hook