issue-blog
issue-blog copied to clipboard
React Hooks 尝鲜
React Hooks 尝鲜
前言
React 在 16.70-alpha 中首次提出 Hooks 这个新特性,并且在 16.8.0 正式发布 Hooks 稳定版本。React Hooks 指的是在 Function Component 中插入一些 Hooks,通过使用这些 Hooks 可以让 Function Component 拥有 state 和生命周期等 React 特性。
为什么会有 React Hooks
React Hooks 要解决的问题是状态逻辑复用,是继 render-props 和 higher-order components 之后的第三种状态共享方案。它主要解决了如下一些痛点:
组件中的状态逻辑难以复用
在一个 React Componet 中,会在 state 中存储状态,并且在组件的各个 lifecycle 函数中执行特有的逻辑,比如在 componentDidMount 去请求数据、在 componentWillUnMount 中卸载实例、取消事件监听器等。这些 state 和 生命周期和组件强耦合,使得 state 和生命函数的逻辑无法抽离得到复用。而 Hooks 可以从组件中提取状态逻辑,从而达到复用。
复杂组件导致的 wrapper hell 和逻辑难以维护
React 的组件带来的好处是模块化,但是当逻辑比较复杂时,就会出现组件嵌套地狱,看看 Devtool 里面的嵌套,是不是有点吓人。
除此之外,复杂组件会在不同的生命周期中执行很复杂的逻辑,比如在 componentDidMount 请求数据,或者在 componentWillReceiveProps 中根据 nextProps 改变组件 state 等等,后期维护和理解的成本会非常高。
再加上 class Component 的 this 指向问题,为了保证指向正确,需要用 bind 绑定或者使用箭头函数,如果没有绑定,就会出现各种 bug。
怎么用 React Hooks
首先使用 class Component 来实现一个最简单的组件:
import React, { useState } from 'react';
import { Button } from 'antd';
function Demo() {
const [count, setCount] = useState(0);
return (
<div>
<Button type="primary" onClick={() => {
setCount(count => count + 1)
}}>Add</Button>
<div className="number">{count}</div>
</div>
)
}
在上面这个例子中,useState 就是一个 Hook,它接受一个参数作为 state 的初始值,返回一个数组,结构为 [value,setValue],其中 value 对应的 state,相当于 Class Component 中的 this.state,setValue 是修改 state 的函数,相当于 class Component 中 this.setState。 其他的就按照 Function Component 写就好了,是不是看起来很清爽?
API 介绍
现在介绍一下 React 内置的几种 Hooks 以及它们的用法。
useState
上面的例子就是用的 useState,当然是最简单的用法,总结一下 useState 的特性,如下:
- 首次渲染时,将传入的参数作为 initialState 使用,当再次 render 时,使用最新的值渲染。如果 initialState 的逻辑比较复杂,可以传入一个函数,并返回计算后的初始值,该函数只在初始渲染时才会被调用。
const [complexState, setComplexState] = useState(()=> {
const initialValue = complexFunction(props);
return initialValue;
})
- 返回一个 state 和更新 state 的函数,且该函数接受一个新的 state 值
- 多个 state 使用多个 useState,并且 useState 之间相互独立。
function Demo() {
const [count, setCount] = useState(0);
const [type, setType] = useState('default')
const [list, setList] = useState([1])
....
}
useEffect
useEffect 是用来执行副作用操作的,通过这个 Hook,可以执行一些组件渲染之后的逻辑,比如事件监听、设置标题等。它相当于是 Class Component 中的 componentDidMount、componentDidUpdate、componentWillUnMount 这三个生命周期的集合。举个最简单的例子,如下:
import React, { useEffect, useState } from 'react';
import axios from 'axios'
function UseEffectDemo() {
const [list, setList] = useState([]);
useEffect(() => {
async function fetchData() {
const result = await axios('https://hn.algolia.com/api/v1/search?query=react');
setList(result.data.hits);
}
fetchData()
}, [])
return <div className="movie-container">
{list.map(item => {
return <div className="item">{item.title}</div>
})}
</div>
}
这个 Hook 的特性如下:
- 组件首次渲染和之后更新的每次渲染都会调用 useEffect。
- 允许传入第二个参数来决定是否执行 effect 里的逻辑,这可以减少一些不必要的性能损耗,如果传入一个 [],则只会在 componentDidMount 和 componentWillUnMount 时期才执行,传入 state,则表示只有当 state 发生改变的时候才触发。如下:
useEffect(() => {
// do something
}, [type]); // 只有 type 发生改变才执行 useEffect 里面的逻辑
- 在 useEffect 中返回一个函数可以执行一些清理操作,比如取消订阅等,这些逻辑会在前一次 effect 执行之后下一次 effect 执行之前以及 componentWillUnMount 的时候执行。如下:
useEffect(() => {
//do something
return function cleanup(){
//do something clean up
}
})
useContext
useContext 主要是为了使用 context,而且不用像以前一样用 Provider、Consumer 包裹组件,可以大大提高代码的简洁性。使用 createContext 实现一个简单的例子,如下:
const themeContext = React.createContext('light')
// App 组件
class App extends React.Component {
state = {
theme: 'red'
}
changeThme = (type) => {
this.setState({ theme: type })
}
render() {
return (<themeContext.Provider value={this.state.theme}>
<Button onClick={this.changeThme.bind(this, 'black')}>黑色</Button>
<Button onClick={this.changeThme.bind(this, 'red')}>红色</Button>
<Consumer />
</themeContext.Provider>);
}
}
// Consumer 组件
class Consumer extends React.Component {
render() {
return <themeContext.Consumer>
{theme => {
return <div>{theme}</div>
}}
</themeContext.Consumer>
}
}
使用 context,就可以避免 props 的多层传递。对上面的例子使用 useContext 进行改造,代码如下:
export const ThemeContext = createContext('light')
// App组件
function UseContextDemo() {
const [theme, setTheme] = useState('red');
return (<ThemeContext.Provider value={theme}>
<Button onClick={() => setTheme('black')}>黑色</Button>
<Button onClick={() => setTheme('red')}>红色</Button>
<Consumer />
</ThemeContext.Provider>);
}
// Consumer 组件
function Consumer() {
// 直接通过 useContext 获取值即可,不需要使用 Context.Consumer 包裹
const theme = useContext(ThemeContext)
return <div>{theme}</div>
}
使用 useContext 可以直接获取值,不需要用 ThemeContext.Consumer 包裹组件。代码看起来更加简洁。
useReducer
useReducer 主要是 useState 的语法糖,主要是针对复杂 state 或者下一个 state 依赖之前 state 的场景,主要有如下特点:
- 和 useState 返回很像,返回 state 和 dispatch 函数
- 接受三个参数,第一个参数是 reducer,类 redux 的 reducer,第二个参数是 initialState,如果你想重置,state,可以传入第三个参数-- init 函数,此时第二个参数作为 init 函数的参数。reducer 的形式如下:
function reducer(state, action) {
switch (action.type) {
case "increment":
return {
...state,
count: count + 1
};
default:
return state;
}
}
举个🌰如下:
const initialState = { count: 0 }
const init = (initialState) => {
return initialState;
}
const reducer = (state, action) => {
switch (action.type) {
case 'ADD':
return { ...state, count: state.count + 1 };
break;
case 'DEC':
return { ...state, count: state.count - 1 };
break;
case 'RESET':
return init(action.payload)
default:
return state;
}
}
function UseReducerDemo() {
const [state, dispatch] = useReducer(reducer, initialState, init)
return (
<div>
<div>Count: {state.count}</div>
<button onClick={() => dispatch({ type: 'ADD' })}>Add</button>
<button onClick={() => dispatch({ type: 'DEC' })}>DEC</button>
// 传入 initialState,进行复位
<button onClick={() => dispatch({ type: 'RESET', payload: initialState })}>RESET</button>
</div>
);
}
如上例子所示,定义一个 reduce 函数,接收一个 state 和 action 参数,返回一个新的 state。通过传递 init 初始化函数,可以对 state 进行复位。
useCallback 和 useMemo
在 Class Component 中,我们可以使用 shouldComponentUpdate 来控制组件重新渲染的条件,从而避免复杂逻辑带来性能性能上的损耗,而在 Function Component 中没有 shouldComponentUpdate 这个生命周期,怎么办?useCallback 和 useMemo 就是用来解决这个问题的。
useCallback 和 useMemo 会在组件首次渲染的时候执行,然后会根据依赖项是否发生改变而再次执行,并且这两个 Hook 都返回缓存的值,useCallback 返回缓存的函数,useMemo 返回缓存的变量。
首先举一个🌰,如下:
function UseMemoDemo() {
const [count, setCount] = useState(1);
const [value,setValue] = useState('')
//第一种,没有使用 useMemo
const computing = () => {
let sum = 0;
for(let i=0;i<count*100;i++){
sum += i;
}
console.log(sum, 'computing')
return sum;
}
// 第二种使用 useMemo
const computing =useMemo( () => {
let sum = 0;
for(let i=0;i<count*100;i++){
sum += i;
}
console.log(sum, 'computing')
return sum;
},[count])
return <div>
<div>Count: {count}</div>
<div> SUM: {expensive}</div>
<button onClick={() => setCount(count + 1)}>Add</button>
<input className="input" onChange={(e)=>setValue(e.target.value)}/>
</div>
}
如上所示:expensive 是计算量很大的函数,并且当 state 发生改变时,组件就会重新渲染,从而导致 computing 函数重新执行,而 computing 只和 count 相关,但是 value 发生改变,computing 还是会重新计算。这是没必要的,所以我们可以使用 useMemo 来控制没必要的执行,第二个参数表示依赖项,只有依赖项发生改变时才会执行。
useCallback 和 useMemo 不同的是,它返回一个缓存的函数,并且 useCallback(fn,deps) = useMemo(()=>fn(),deps),它有什么作用呢,举一个例子:
function UseCallbackDemo() {
const [count, setCount] = useState(0);
const [value, setValue] = useState('')
// 第一种,没有使用 useCallback
// const callback = () => {
// return count + 1;
// }
// 第一种,没有使用 useCallback
const callback = useCallback(() => {
return count + 1
}, [count]);
return (<div>
<div>Parent Count: {count} </div>
<button onClick={() => { setCount(count + 1) }}>Add</button>
<input className="input-container" onChange={(e) => { setValue(e.target.value) }} />
<Child callback={callback} />
</div>)
}
// Child 组件
function Child(props) {
const [value, setValue] = useState(0)
useEffect(() => {
setValue(props.callback())
},[props.callback])
return <div>Child Count: {value}</div>
}
如上所示,父组件将 callback 函数传递给子组件,然后子组件在 useEffect 中判断 callback 是否发生改变,从而更新自身的 state,当父组件的 value state 发生改变是,并不会触发 useEffect 的更新操作,所以使用 useCallback 可以避免子组件不必要的重复渲染。
useRef
useRef 相当于是一个存储属性的地方,它在组件的整个生命周期内都保持不变,它的特性如下:
- 接受一个参数,并且作为属性 current 的初始值。
- 比 ref 更有用,可以存储任何可变值。
- 当 ref 的 current 属性变化时,不会触发组件的重新渲染
它的用法如下:
function UseRefDemo() {
const inputRef = useRef(null)
const onClick = () => {
inputRef.current.focus()
}
return <div>
<input ref={inputRef}></input>
<button onClick={onClick}>Focus</button>
</div>
}
如上,在点击 button 时,设置 inputRef 获取焦点,其中 input 这个实例就保存在 inputRef 中。
useImperativeHandle
useImperativeHandle 用于自定义暴露给父组件的 ref 属性,该 hooks 需要和 forwardRef 一起使用,例子如下:
function UseImperativeHandleDemo() {
const parentRef = createRef()
return (<div>
<Child ref={parentRef} />
<br />
<button onClick={() => { parentRef.current.focus() }} >获取焦点</button>
</div>)
}
//Child 组件
import React, { useRef, useImperativeHandle, forwardRef } from 'react';
const Child = (props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}))
return <input ref={inputRef} />
}
export default forwardRef(Child)
如上面所示,父组件可以直接调用在 Child 里面定义的 Ref.current.focus 方法。
useLayoutEffect
useLayoutEffect 和 useEffect 类似,两者不同的地方是:
- useEffect 是被 react-scheduler 调度,异步执行,不会阻塞浏览器的渲染
- useLayoutEffect 在 DOM 变更之后同步执行,比较适用于 DOM 更新后,立刻去执行变更 DOM 副作用的场景,和以往的 componentDidMount 和 componentDidUpdate 的表现一致。
useDebugValue
用户在 react devtools 中显示 hooks 属性,第二个参数可以进行格式化能力,例子如下:
const [date] = useState(new Date());
useDebugValue(date, date => date.toDateString());
自定义 Hooks
除了上面提到的官方已有的 hooks,我们还可以自定义 hooks,通过自定义,可以将组件逻辑提取到可重用的函数中。并且自定义的 hooks 之间也是相互独立的,举个例子:
function CustomHookDemo() {
const [hoverRef, isHover] = useHover();
return <div ref={hoverRef}>{isHover ? 'I am hovered' : 'I am not hovered'}</div>
}
//useHover 的 hook
import React, { useState, useRef, useEffect } from 'react';
const useHover = () => {
const [isHover, setIsHover] = useState(false)
const ref = useRef(null);
const handleMouseOver = () => {
setIsHover(true)
}
const handleMouseOut = () => {
setIsHover(false)
}
useEffect(() => {
const node = ref.current;
if (node) {
node.addEventListener('mouseover', handleMouseOver);
node.addEventListener('mouseout', handleMouseOut);
return () => {
node.removeEventListener('mouseover', handleMouseOver);
node.removeEventListener('mouseout', handleMouseOut);
};
}
}, [ref.current]);
return [ref, isHover]
}
export default useHover
如上所示,我们把 hover 的逻辑抽离到 useHover 这个 hook 中,并且把 hover 的 DOM 实例和 isHover 的值返回,这样其他组件想要这个逻辑就可以直接复用。
自定义 Hooks,需要遵循如下规则:
- 自定义 hook 是一个函数,必须以 use 开头
- 自定义 hook 可以调用官方提供的 Hooks
使用 Hooks 需要注意的点
虽然 Hooks 比较强大,但是在使用过程中,还是有一些点需要注意,比如:
- 只在顶层使用 Hook,不在循环、条件或者嵌套函数中调用 hook
- 只在 React 函数和 自定义 Hooks 中使用,不要在普通 js 中使用 Hooks