自定义 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
}