React Hooks 是 React 16.8 新增的特性,它可以让你在不编写 class 的情况下使用 state 以及其它的 React 特性。
首先来看一下使用 Hooks 编写的代码是什么样子的:
import React, { useState, useEffect } from ‘react’;
export default () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = count;
});
return (
<div>
<p>Current count is: {count}</p>
<button onClick={() => setCount(count + 1)}>increment count</button>
</div>
);
};
这段代码实现了一个简单的计数器功能,点击按钮使 count 增加,同时使页面标题的显示与 count 的变化同步。这里引入了 useState
与 useEffect
,我们稍后再来介绍他们,先来对比一下与 class component 的区别:
import React, { Component } from ‘react’;
export default class extends Component {
state = {
count: 0,
};
componentDidMount() {
document.title = this.state.count;
}
componentDidUpdate() {
document.title = this.state.count;
}
render() {
return (
<div>
<p>Current count is: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
increment count
</button>
</div>
);
}
}
通过对比发现使用 Hooks 编写代码简练多了,state 初始化及变更 state 的方式发生了改变,生命周期函数也不用再编写了,Hooks 使我们拥有了在函数组件里使用 state 及生命周期的能力。React 的组件分类也进行了重新定义,以前是有状态组件、无状态组件,现在划分为了类组件及函数组件。
那么接下来就来详细分析一下 Hooks 的具体用法。
useState
首先是 useState
,这个是干什么用的?useState
可以让我们在函数组件内部使用 state 进行状态管理,语法如下:
// state, 和 setState 可以任意命名,比如 count, setCount
const [state, setState] = useState(initialState)
useState
接收一个初始的状态 initialState
,它返回一个数组,进行解构后一个是 state,一个是更新 state 的函数 setState
,每次 setState
调用后,就会将组件的一次重新渲染加入队列(重新渲染组件或者和其它的 setState 进行批量更新)。
initialState
作为初始状态,可以是基本类型,也可以是对象(例如{a:1}
)或者函数,它只在组件首次渲染时被用到,首次渲染后 state 与 initialState 相同。
在每次重新渲染后,useState
都会返回最新的 state
。而 setState
就相当于是类组件中的 this.setState()
,但是 Hooks 里的 setState 并不会进行新 state 与旧 state 的合并,而是直接覆盖。
另外 useState()
接收的参数也是任意的,可以是基本类型,也可以是对象,还可以传入一个函数,在这个函数里可以拿到旧的 state,例如:
{/* <button onClick={() => setCount(count + 1)}>increment count</button> */}
<button onClick={() => setCount((oldCount) => oldCount + 1)}>
increment count
</button>
useState
也可以使用多次,进行多个状态管理:
const [count, setCount] = useState(0);
const [color, setColor] = useState('red');
useEffect
那么 useEffect
又是干什么的呢?useEffect
可以让我们在函数组件中进行副作用操作,比如获取数据、DOM 操作、日志记录等都属于副作用。
useEffect
可以看做是 componentDidMount
, componentDidUpdate
, componentWillUnmount
这三个生命周期函数的组合。
例如在前面的例子中我们使用 useEffect
来更新页面标题:
useEffect(() => {
document.title = count;
});
每次使用 setCount
更新状态,useEffect 里的函数都执行了一次,这就相当于是在类组件中同时使用了 componentDidMount
及 componentDidUpdate
:
componentDidMount() {
document.title = this.state.count;
}
componentDidUpdate() {
document.title = this.state.count;
}
可以看到,在两个生命周期里写了同样的逻辑,这无疑造成了代码的冗余,而 useEffect
则会在每次渲染之后都执行,包括首次渲染,这样我们把类似于示例中的相同操作编写一次放在 useEffect 中即可。
对于上面示例中的副作用是无需清除的,但是还有些副作用是需要清除的,例如手动绑定的 DOM 事件。
在类组件中清除副作用可以在 componentWillUnmount
生命周期方法中进行,那么在 Hooks 里该如何实现呢?
其实,useEffect
已经提供了清除的机制,每个副作用都可以添加一个可选的返回函数,用来在组件卸载的时候进行清除操作,比如下面代码:
import React, { useState, useEffect } from ‘react’;
export default () => {
const [size, setSize] = useState({ width: 0, height: 0 });
const handleResize = () => {
const win = document.documentElement.getBoundingClientRect();
setSize({ width: win.width, height: win.height });
};
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
});
return (
<div>
<p>Current size is:</p>
<p>
width: {size.width}, height: {size.height}
</p>
</div>
);
};
另外使用 useEffect
还有一个问题,就是每次进行 setState 操作时,可能都会触发副作用的执行, 即使更改的 state 和这个副作用并没有关系,比如下面的代码:
import React, { useState, useEffect } from ‘react’;
export default () => {
const [count, setCount] = useState(0);
const [color, setColor] = useState('red');
useEffect(() => {
console.log('count');
document.title = count;
});
return (
<div>
<p style={{ color: color }}>Current count is: {count}</p>
<button onClick={() => setCount(count + 1)}>increment count</button>
<br />
<button onClick={() => setColor(color === ‘red’ ? ‘blue’ : ‘red’)}>
switch color
</button>
</div>
);
};
我们发现在改变颜色时,修改页面标题的副作用也在反复执行,这无疑造成了资源的浪费,更不是我们想要的效果。当然,useEffect
给我们提供了第二个参数,可以对渲染进行控制,它接收一个数组作为参数,可以传入多个值,我们来对上面代码进行改进:
useEffect(() => {
console.log('count');
document.title = count;
}, [count]);
我们传入了 [count]
作为第二个参数,这样在每次渲染时都会先对 count 的新值和旧值进行对比,只有变化的时候这个副作用才会执行。
另外使用 Hooks 还需要遵循如下规则:
- 只在函数组件中使用 Hooks,不要在普通函数中使用
- 只在最顶层使用 Hooks,不要在条件、循环或者嵌套函数中使用 Hooks
当然为了保证这些规则,我们可以使用 ESLint 插件 eslint-plugin-react-hooks
进行约束。
useContext
首先看一下 Context 是怎么干什么用的:Context 提供了一种跨层级传递数据的方式,有了它就无需在每层组件都手动传递 props 了。
Context API 使用示例
通过下面例子回顾一下 Context API 的使用:
// 创建一个 Context 对象
const ThemeContext = React.createContext(‘light’);
const ContextAPIDemo = () => {
return (
// 每个 Context 对象都会返回一个 Provider React 组件,
// 它接收一个 value 属性,传递给消费组件,
// 允许消费组件订阅 context 的变化
<ThemeContext.Provider value="dark">
<MiddleComponent />
</ThemeContext.Provider>
);
};
// 一个中间组件,使用 Context 传递数据并不需要中间组件透传
const MiddleComponent = () => {
return (
<div>
<ThemedButton />
</div>
);
};
class ThemedButton extends Component {
// 把创建的 ThemeContext 对象赋值给 contextType 静态属性,
// 这样就可以通过 this.context 访问到 ThemeContext 里面的数据
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
const Button = (props) => {
const btnBgColor = props.theme === ‘dark’ ? ‘#333’ : ‘white’;
return <button style={{ backgroundColor: btnBgColor }}>Toggle Theme</button>;
};
这里使用 React.createContext()
创建了一个 ThemeContext
,然后通过 Provider 把数据传递给消费组件,即 ThemedButton
,最后在消费组件里把创建的 ThemeContext
赋值给了 contextType
静态属性,这样我们在消费组件里就可以通过 this.context
的方式访问数据了。
当然在消费组件里还可以使用 Consumer
来获取 Context 的数据:
const ThemedButton = () => {
return (
<ThemeContext.Consumer>
{(theme) => <Button theme={theme} />}
</ThemeContext.Consumer>
);
};
Ok,做了简单的回顾之后,就来看一下在 Hooks 里该如何使用 Context。
useContext 的使用
接下来我们使用 useContext
改造下上面的示例:
// 省略其它代码…
const Button = () => {
const theme = useContext(ThemeContext);
const btnBgColor = theme === ‘dark’ ? ‘#333’ : ‘white’;
return <button style={{ backgroundColor: btnBgColor }}>Toggle Theme</button>;
};
改造后就完全可以不用 Consumer
或 contextType
静态属性了,哪里需要哪里就直接使用 useContext
即可。
useContext
接收一个 Context 对象作为参数,返回的是通过 Provider 传入的 value 数据。
由于示例中一直是一个固定的 ThemeContet,如果需要一个动态的 Context 该怎么办?我么可以通过 Provider
的 value
传入回调函数进行处理:
const ContextAPIDemo = () => {
const [theme, setTheme] = useState(initialState.theme);
const toggleTheme = (val) => {
setTheme(val === 'dark' ? 'light' : 'dark');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<MiddleComponent />
</ThemeContext.Provider>
);
};
然后在目标组件可以通过 useContext
拿到回调函数,例如:
const Button = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
const btnBgColor = theme === ‘dark’ ? ‘#333’ : ‘white’;
return (
<button
style={{ backgroundColor: btnBgColor }}
onClick={() => toggleTheme(theme)}
>
Toggle Theme
</button>
);
};
useReducer
提起 Reducer,用过 Redux 的同学应该都不陌生,Reducer Hook 里的 Reducer 其实跟 Redux 里的 Reducer 是同一个意思,接收旧的 state,返回新的 state,形如:(state, action) => newState
。
useReducer
的基本语法是这样的:
const [state, dispatch] = useReducer(reducer, initialArg, init);
它接收三个参数:reducer
, initialArg
及可选的 init
函数:
reducer
: 接收旧的 state,返回新的 state,形如:`(state, action) => newStateinitialArg
: 初始 stateinit
: 作为一个函数传入,惰性地初始化 state,state 将会被设置为init(initialArg)
,这样可以将计算 state 的逻辑提取到外部,同时如果有重置 state 的需求的话也会很方便
下面就来看一下具体的用法:
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case ‘increment’:
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: ‘increment’ })}>+</button>
</>
);
};
useMemo
useMemo
的语法是这样的:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
接收一个函数和依赖数组作为参数,然后只在依赖项变化时才会计算 memoizedValue
的值,基于这一点,我们可以作为性能优化的一个手段。
例如下面代码,我们通过对 computedNum
这个计算方法添加了 useMemo
,把 count
作为依赖,只有 count
变化时 computedNum
才会计算,这样可以防止无关的操作也会引起函数的执行(如果不加,在点击 change color
时,computedNum
也会反复计算)。
export default () => {
const [count, setCount] = useState(0);
const [color, setColor] = useState(‘blue’);
const computedNum = useMemo(() => {
console.log('render when count change');
return count + 1;
}, [count]);
return (
<div>
<p>Current color is: {color}</p>
<p>Current count is: {count}</p>
<p>Computed num is: {computedNum}</p>
<button onClick={() => setCount(count + 1)}>increment count</button>
<button onClick={() => setColor(color === ‘blue’ ? ‘green’ : ‘blue’)}>
change color
</button>
</div>
);
};
当然,我们也可以用 useMemo
去包裹一个组件,从而实现类似 PureComponent
的效果,防止组件反复的渲染。
<>
{useMemo(
() => (
<ColorDemo color={color} />
),
[color]
)}
<>
useCallback
useCallback
用法如下:
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useCallback
返回了一个 memoized 回调函数,它跟 useMemo
类似,useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
。它的第二个参数跟 useMemo
的一样,传入一个依赖项数组,只有依赖变化时才会生成新的函数,否则一直是同一个函数,这在进行组件性能优化时非常有用,避免多余的开销。比如使用 props 传递函数时,使用 callback
可以避免每次渲染都生成一个新的函数,示例如下:
let firstOnClick;
const Button = (props) => {
if (!firstOnClick) {
firstOnClick = props.onClick;
}
console.log(firstOnClick === props.onClick);
return <button onClick={props.onClick}>increment count</button>;
};
export default () => {
const [count, setCount] = useState(0);
const [color, setColor] = useState('blue');
// 不用 useCallback 包裹的话,点击 change color 按钮,Button 组件里每次也都会生成新的函数
// const handleIncrementCount = () => {
// setCount(count + 1);
// };
// 使用 useCallback 包裹后,只有点击 increment count 时 count 发生变化,才会生成新的函数,
// 修改颜色并不会生成新函数
const handleIncrementCount = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<p>Current color is: {color}</p>
<p>Current count is: {count}</p>
<Button onClick={handleIncrementCount} />
<button onClick={() => setColor(color === 'blue' ? 'green' : 'blue')}>
change color
</button>
</div>
);
};
useRef
useRef
会返回一个可变的 ref 对象,其.current
属性被初始化为传入的参数(initialValue),返回的 ref 对象在组件的整个生命周期内保持不变。
通常它被用来获取子组件(或 DOM 元素):
export default () => {
const btnRef = useRef(null);
useEffect(() => {
btnRef.current.addEventListener('click', () => {
alert('click me');
});
}, []);
return (
<div>
<button ref={btnRef}>click</button>
</div>
);
};
另外,由于 useRef
在每次渲染时返回的都是同一个 ref 对象,因此可以用 ref.current
保存一个变量,它不会随着组件的重新渲染而受到影响,例如下面一个定时器的例子:
export default () => {
const [count, setCount] = useState(0);
const ref = useRef(0);
useEffect(() => {
const timer = setInterval(() => {
console.log('interval');
setCount(++ref.current);
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<div>
<p>Current count is: {count}</p>
</div>
);
};
同时,变更 .current
属性也不会引起组件的重新渲染。
useImperativeHandle
useImperativeHandle
可以让我们在使用 ref 时自定义暴露给父组件的实例值,这样可以隐藏掉一些私有方法或属性,下面是一个 useImperativeHandle
与 forwardRef
配合使用的例子:
function Input(props, ref) {
const [val, setVal] = useState(0);
useEffect(() => {
setVal(props.count);
}, [props.count]);
const clearInput = useCallback(() => {
setVal('');
}, []);
useImperativeHandle(ref, () => ({
clear: () => {
clearInput();
},
}));
return (
<input type="text" value={val} onChange={(e) => setVal(e.target.value)} />
);
}
const FancyInput = forwardRef(Input);
export default () => {
const [count, setCount] = useState(0);
const fancyRef = useRef(null);
const handleClearInput = useCallback(() => {
fancyRef.current.clear();
}, []);
return (
<div>
<p>Current count is: {count}</p>
<button onClick={() => setCount(count + 1)}>increment count</button>
<hr />
<FancyInput ref={fancyRef} count={count} />
<button onClick={handleClearInput}>clear input</button>
</div>
);
};
useLayoutEffect
useLayoutEffect
与 useEffect
用法相同,只不过 useLayoutEffect
会在所有 DOM 更新之后同步调用,可以使用它读取 DOM 布局并同步触发重渲染,但是还是建议使用 useEffect
,以避免阻塞 UI 更新。
这里有个示例,使用useEffect
和 useLayoutEffect
会有明显的差异:
const useLayoutEffectDemo = () => {
const [height, setHeight] = useState(100);
const boxRef = useRef(null);
useLayoutEffect(() => {
if (boxRef.current.getBoundingClientRect().height < 200) {
console.log('set height: ', height);
setHeight(height + 10);
}
}, [height]);
const style = {
width: '200px',
height: `${height}px`,
backgroundColor: height < 200 ? 'red' : 'blue',
};
return (
<div ref={boxRef} style={style}>
useLayoutEffect Demo
</div>
);
};
我们相当于给盒子设置了粗糙的过渡变化,使用 useEffect
这种过渡是生效的,但是换成 useLayoutEffect
之后,过渡效果已经没了,只会显示最终的效果。也就是说在浏览器执行绘制之前,useLayoutEffect
内部的更新计划将被同步刷新。
useDebugValue
用于开发自定义 Hooks 调试使用,例如:
useDebugValue(
size.width < 500 ? '---- size.width < 500' : '---- size.width > 500'
);
可以在 React DevTools 里查看相关的信息输出。
自定义 Hooks
React Hooks 的强大,不仅仅是因为官方的内置 Hooks,同时它还支持自定义 Hooks,提高了组件的复用性,从一定程度了取代了 HOC 和 render props 的复用方式。
自定义 Hook 需要以 use
开头,其内部还可以调用其他的 Hook,自定义 Hook 不需要具有特殊的标识,我们可以自定义参数及返回值,下面是一个自定义 Hook 示例,自定了一个 useSize
,用来获取窗口的大小。
import { useState, useCallback, useEffect } from 'react';
const getDomElInfo = () => document.documentElement.getBoundingClientRect();
const getSize = () => ({
width: getDomElInfo().width,
height: getDomElInfo().height,
});
export default function useSize() {
const [size, setSize] = useState(() => getSize());
const onChange = useCallback(() => {
setSize(() => getSize());
}, []);
useEffect(() => {
window.addEventListener('resize', onChange, false);
return () => {
window.removeEventListener('resize', onChange, false);
};
}, [onChange]);
return size;
}
使用这个自定义 Hook 也很简单:
import useSize from './useSize';
export default () => {
const size = useSize();
return (
<div>
<p>
size: width-{size.width}, height-{size.height}
</p>
</div>
);
};
好了,关于 React Hooks 就介绍到这里,它不仅仅打破了原有的组件编写方式,更是一种新的思维, 非常值得学习。