从React 16.8版本开始,新增的React hooks可以让用户在不用创建react class的同时可以使用state等react属性;
本文部分内容翻译自React Hooks官方文档
Hooks are functions that let you “hook into” React state and lifecycle features from function components. Hooks don’t work inside classes — they let you use React without classes. (We don’t recommend rewriting your existing components overnight but you can start using Hooks in the new ones if you’d like.)
引入hooks可以将很多之前以类的形式表现的组件以函数的形式呈现,也不必从React
中引入Component
,不用再使用冗余的class
语法,代码结构更加简洁;
引入hooks之后,可以将React
的state
、props
的形式抽离出去,需要的时候再使用hooks
将这些外部功能引入,能够使得组件尽量以纯函数的形式呈现,从而提高了组件的复用性;
简化了部分结构,比如多个子组件共享状态,使用hooks可以更好地进行状态共享,而不是像之前给多个子组件分别以props
的方式进行传值;
如useState
等hooks,新引入的hooks用于函数式组件中,不用再关心this指向,this绑定问题及由此带来的各种bug,同时代码更加精简;
React Conf 2018上指出,提出React hooks
出于以下三个原因:
render props
和HOC
,这也需要你重新组织你的组件结构,并且使代码变得难理解和维护;并且由providers
、consumers
、高阶组件、render props
等组成的组件会形成嵌套地狱。所以,react需要为共享状态逻辑提供更好的原生途径componentDidMount
、componentDidUpdate
、componentWillUnmount
等;当组件变得庞大时,会有很多状态逻辑和副作用;使用class组件时很多相关的逻辑会拆分到不同的生命周期函数中,而不相关的一些逻辑又会放在同一个方法中;这样逻辑不一致也变得难以维护,因为修改某个逻辑往往要查看多个地方的相关联逻辑;Hook
不会强制根据生命周期进行划分,可以将组件相关联的部分拆分为更小的函数,便于逻辑统一与管理;this
的指向问题,需要进行事件绑定等,这增加了很多冗余代码;另外,class组件不能很好的压缩, 也会出现HMR不稳定的问题;根据官方文档,引入hooks的React新版本时向后兼容的,并且不会对用户之前对React建立的理解和使用造成困扰。另外,官方目前并没有将class
移出React的计划,因此是否学习React Hooks
—— It depends on you!
React Hooks
使用了JavaScript
闭包机制,因此可以直接访问函数组件中的state
或props
;
每个组件内部会维护一个hooks
的列表,当列表中的一个hook
被使用后,指针就会移动到下一个位置;因此需要保证Hooks
的调用规则:只在顶层调用;不能在条件、循环、嵌套函数中调用;
useState
useState
是React新引入的hook(钩子函数),可以在函数式的组件中调用,用于设置和更新状态,作用类似于之前版本中的this.state
及this.setState()
;useState
接受一个state
的初始值作为参数;返回值为一个state
和一个设置state的函数,通常以set
加state
命名;这样,useState
将状态值的初始化、读取及更新集于一身。useState
;const [ count, setCount ] = useState(0);
const [ color, setColor ] = useState('red');
// counter demo
import React, {
useState } from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [ count, setCount ] = useState(0); // 设置state.count的初始值为0;
return <button onClick={
() => setCount(count + 1)}>{
count}</button>
}
useEffect
介绍:根据官方文档介绍,数据获取、事件订阅、DOM操作等可能会影响到其他组件并且不能在组件渲染过程中进行,这些操作被称之为side-effects
或Effects
。useEffect
钩子函数让我们可以在函数式组件中引入这些副作用,作用等同于class
中使用的一些生命周期函数的,如componentDidMount
、componentDidUpdate
、componentWillUnMount
。 默认情况下,React在每次render
后会运行Effects
;由于事件订阅(如定时器)等如果运行Effects
之后不及时清理,会造成内存泄漏。基于此,Effects
被分为需要清理的Effects
及不需要清理的Effects
;
不需要清理的Effects
:此时useEffect
等效于componentDidMount + componentDidUpdate
;
需要清理的Effects
:此时useEffect
等效于componentDidMount + componentDidUpdate + componentWIllUnMount
;
特点:由于React的生命周期函数中并没有一个函数可以让我们在每次render
之后执行某些操作,使用class
要达到这样的目的,需要我们使用至少两个生命周期函数componentDidMount
及componentDidUpdate
(componentDidMount
仅在第一次渲染后执行,componentDidUpdate
则是在第一次挂载后更新时才会执行);
使用useEffect
之后,逻辑更加清晰,很多操作不用再分析到底是要在componentDidMount
还是componentDidUpdate
中执行;
useEffect
通常在React的函数式组件中调用,这样可以直接使用state
及props
等;
useEffect
接收一个函数作为参数,该参数为我们想要在render
之后进行的操作,即所谓的Effects
;
与componentDidMount
及componentDidUpdate
不同,useEffect
不会阻塞浏览器更新页面;
useEffect
中的副作用函数每次都会被重新派发,从而避免被停滞;
当使用需要清理的Effects
时,useEffect
可以返回一个函数,该函数中可以定义如何清理副作用。react会在需要清理该副作用时自动调用该函数;
使用多个useEffect
可以实现关注点分离:传统的class中生命周期函数常常包含不相关的逻辑,而相关的逻辑又分布在不同的生命周期函数中,使用多个useEffect
可以解决这一问题;
性能
有些操作我们只想在componentDidMount
执行一次,而useEffect
会每次渲染都执行。类似的情况很多,而跳过不必要的Effects
可以节省性能开销。
在react类组件中,我们在使用componentDidUpdate
时可以使用以下方法:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${
this.state.count} times`;
}
}
useEffect
接受第二个数组形式的参数传入上一个state值,使用useEffect
则可以使用以下方法:
useEffect(() => {
document.title = `我会加倍奉还:${
count * 2}次`;
}, [count])
如果第二个参数传入空数组,则表示该组件卸载时才进行解绑操作;
demo
// counter demo
import React, {
useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
useEffect(() => {
document.title = `我会加倍奉还: ${
count*2}`;
})
const App = () => {
const [ count, setCount ] = setState(0); // 设置state.count的初始值为0;
return <button onClick={
() => setCount(count + 1)}>{
count}</button>
}
/*
* 如果effect频繁变化怎么办?
*/
// 错误示例
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count+1); // 由于Hook的执行时创建一个闭包,count初始值一直为0;count的值永远不会超过1;
}, 1000);
return () => clearInterval(id);
}, []); // Bug: `count`未指定为依赖,只会在初始化时执行;指定count依赖能修复这个bug,但会导致每次更新时定时器会被重置。
}
// 推荐方案
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // 使用setState的函数式更新方式,函数方式也可以避免重新创建`useState`的初始值;
}, 1000);
return () => clearInterval(id);
}, []);
}
你可以从依赖中去除dispatch
,setState
,和useRef
包裹的值,React会确保它们是静态的。
useContext
使用context
我们可以为一些子组件共享数据,并避免通过中间组件传值;所有的后代组件都可以使用共享数据;
import React, {
useContext } from "react";
import ReactDOM from "react-dom";
const themes = {
light: {
background: "#eee",
foreground: "#000"
},
dark: {
background: "#000",
foreground: "#fff"
}
};
const ThemeContext = React.createContext(themes.light); // 传入默认值
const App = () => {
return (
<ThemeContext.Provider value={
themes.dark}>
<ToolBar />
</ThemeContext.Provider>
);
};
const ToolBar = () => {
return (
<div>
<ThemeButton />
</div>
);
};
const ThemeButton = () => {
const theme = useContext(ThemeContext);
return <button style={
{
color: theme.foreground, backgroundColor: theme.background}}>Styled by theme context.</button>;
};
ReactDOM.render(<App />, document.getElementById("root"));
useReducer
useReducer
功能与useState
功能类似,但比其功能更加强大。如果你有复杂的状态逻辑需要处理,如下一个状态值依赖于先前的状态值,那么使用useReducer
是比useState
更好的选择。另外,对于多层嵌套传值的情况,使用useReducer
可以dispatch
代替callbacks
进行性能优化。
useReducer
可以进行状态解耦,将useEffect
和一些非强相关的状态解耦出来,更新逻辑由reducer
统一处理;
This is why I like to think of useReducer as the “cheat mode” of Hooks. It lets me decouple the update logic from describing what happened. This, in turn, helps me remove unnecessary dependencies from my effects and avoid re-running them more often than necessary.
译:这就是为什么我倾向于认为useReducer
是Hooks
的“作弊模式”,它能够让我讲更新逻辑和描述发生了什么进行解耦。这样转而可以帮助我移除effects
中不必要的依赖,并且避免很多必要的重复执行。
使用方式
const [ state, dispatch ] = useReducer(reducer, initialArg, init);
其中参数reducer
即redux
中的概念,是一个纯函数,根据action.type
返回新的state
;
第二个参数是一个包含初始值的对象;
第三个参数可选,是用于初始化的函数,参数为第二个参数;
如果使用前两个参数,那么初始值是固定的,如果使用三个参数,那么初始值可以通过外部传入;
demo
demo演示见codesandbox.io;
传入useReducer
第三参数的demo演示见codesandbox.io
import React, {
useReducer } from 'react';
import ReactDOM from 'react-dom';
const initialState = {
count: 0}
const reducer = (state, action) => {
let newState = JSON.parse(JSON.stringify(state));
switch (action.type) {
case 'increment':
newState.count = state.count + 1;
break;
case 'decrement':
newState.count = state.count - 1;
break;
default:
break;
}
return newState;
}
const App = () => {
const [ state, dispatch ] = useReducer(reducer, initialState);
return (
<div>
<button onClick={
() => dispatch({
type: "increment"})}>increment</button>
<span style={
{
padding: "5px"}}>{
state.count}</span>
<button onClick={
() => dispatch({
type: "decrement"})}>decrement</button>
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
useRef
用法
const refContainer = useRef(initialValue);
useRef
返回一个可变对象,它的current属性的初始值为initialValue
,该对象在组件的整个生命周期中均保持不变;
useRef
的本质就是可以在其.current
属性中保存可变值的一个盒子,可以保存任何可变值;变更.current
不会引发组件重新渲染;
demo
demo1地址
// ref获取DOM, demo地址https://codesandbox.io/s/blissful-burnell-22wmc
import React, {
useRef } from "react";
const Demo = () => {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus();
};
const onButtonClick2 = () => {
inputEl.current.value = "input changed";
};
return (
<>
<input ref={
inputEl} />
<button onClick={
() => onButtonClick()}>Focus</button>
<button onClick={
() => onButtonClick2()}>Change Input</button>
</>
);
};
export default Demo;
// demo2 —— 定时器清理
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
}, []);
}
如果只是想设定一个循环计时器,其实不需要ref
也可以的,本身在useEffect
中id
一直存在的;
但是,如果想要在一个事件处理器中清除这个循环定时器的话;使用useRef
就很有用了
function hnadleCancelClick() {
clearInterval(intervalRef.current);
}
useContext
结合useReducer
代替redux
demo具体见codesandbox.io,源自技术胖react hooks文档
useMemo
,缓存方法 —— useCallback
useMemo
通常返回一个memoized
值,入参是一个函数,返回的是函数执行结果并缓存,仅在变更时才更新值;通常用语缓存计算量较大的函数结果;
useMemo
的入参函数实在渲染期间运行的,所以不要在 useMemo
中做一些通常不会在渲染期间做的事;
useCallback
通常返回一个memoized
函数,仅在函数的某个依赖发生变化时才会更新;当我们需要将函数传递下去并且在子组件的useEffect
中调用它时,可以使用useCallback
;
const memoizedValue = useMemo(() => computedExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(() => {
doSomething();
}, [a, b]);
// demo2——父组件向子组件传递函数
function Parent() {
const [query, setQuery] = useState('react');
// ✅ Preserves identity until query changes
const fetchData = useCallback(() => {
const url = 'https://hn.algolia.com/api/v1/search?query=' + query;
// ... Fetch data and return it ...
}, [query]); // ✅ Callback deps are OK
return <Child fetchData={
fetchData} />
}
function Child({
fetchData }) {
let [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, [fetchData]); // ✅ Effect deps are OK
// ...
}
当我们需要在两个JS函数之间共享逻辑时,可以使用自定义的hook函数,从而避免编写过多的重复代码;自定义的hook其参数返回值等都可以自定义,并且其内部可以调用其他的React Hooks
;自定义的hook均以use
作为函数名前缀;
自定义 Hook 更像是一种约定而不是功能。如果函数的名字以 “use” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。 useSomething 的命名约定可以让我们的 linter 插件在使用 Hook 的代码中找到 bug。
demo参见codesandbox.io
import React, {
useState, useEffect, useCallback } from "react";
import "./styles.css";
const useWinSize = () => {
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
const onResize = useCallback(() => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
}, []);
useEffect(() => {
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, [onResize]);
return size;
};
const Demo = () => {
const size = useWinSize();
return <div>页面size:{
`width:${
size.width}, height: ${
size.height}`}</div>;
};
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Demo />
</div>
);
}
React如何知道哪个state
对应哪个useState
?
A: 通过保证Hook
在每次渲染中都是相同的调用顺序;因此不能在条件、循环等场景使用React Hooks
且必须要在顶层调用;
如何有条件地执行Effect
?
A: 可以在Hook
内部最顶层加入判断;
如何测量DOM节点(获取DOM节点位置或者大小等信息)
A:使用callback ref
—— 如果想要在react绑定或者解绑DOM节点的ref
时运行某些代码,可以使用callback ref
;
function MeasureExample() {
const [height, setHeight] = useState(0);
const measureRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={
measureRef}>Hello, world</h1>
<h2>The above header is {
Math.round(height)}px tall</h2>
</>
);
}
从Class
迁移到Hook
,生命周期方法如何对应
construtor
: 函数组件不需要,初始化state
使用useState
;对于初始化计算量大的操作,可以传一个函数给useState
;shouldComponentUpdate
: 借助React.memo
;React.memo
更类似与PureComponent
,它只会对props
进行浅比较(不会比较state
)componentDidMount
、componentDidUpdate
、componentWillUnmount
:使用useEffect
可涵盖;render
:即函数组件本身;获取上一轮props
或state
的值
可以通过ref
手动实现(demo
来自react官网文档),实现代码可参考此处
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevConutRef.current = count; // useEffect发生在渲染后,所以可以拿到最新值;
});
const prevCount = prevCountRef.current; // 保存更新前的count
return <div> Now: {
count}, before: {
prevCount} </div>
}
抽象为自定义hook
:
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return <div> Now: {
count}, before: {
prevCount} </div>
}
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
惰性创建昂贵的对象
state
创建昂贵时,使用函数初始化const [pos, setPos] = useState(createPos(props.count);
// 创建代价高时的推荐写法
const [pos, setPos] = useState(() => createPos(props.count);
useRef
的初始值useRef
不像useState
能够接受一个特殊的函数重载,需要自己编写函数function Image(props) {
const ref = useRef(null);
// IntersectionObserver只会被创建一次
function getObserver() {
if (ref.current === null) {
ref.current = new IntersectionObserver(onIntersect);
}
return ref.curren;
}
// 然后在需要时,调用getObserver()
}
Hook 会因为在渲染时创建函数而变慢吗?
不会。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。
除此之外,可以认为 Hook 的设计在某些方面更加高效:
Hook 避免了 class 需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。
符合语言习惯的代码在使用 Hook 时不需要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中非常普遍。组件树小了,React 的工作量也随之减少。
如何避免向下多层传入回调:
可以使用useReducer
向下传递一个dispatch
函数;
state
与useEffect
的区别?
state
的每次渲染总是获取到最新的状态值,而不是当次渲染对应的特定值;而hooks
每次渲染都有自己的effect
,每个effect
中获取的总是当次渲染对应的state
和props
;
effect
会在每次渲染之后才运行,并且概念上它是组件输出的一部分,可以“看到”某次特定渲染的props
和state
;
每一个组件内的函数(包括事件处理函数、effects、定时器或者API调用等等)会捕获某次渲染中定义的props
和state
;
以下是demo对比:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${
count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {
count} times</p>
<button onClick={
() => setCount(count + 1)}>Click me</button>
</div>
);
}
// 输出结果为0/1/2/3/4/5,其中0是首次渲染的结果;
/**
* state/componentDidMount/componentDidUpdate
*/
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${
count} times`);
}, 3000);
}
// 输出结果0 5 5 5 5 5,因为state总是获取的是最新状态值;
useEffect
的清理时机
由于每次渲染都有对应的effect,所以useEffect
会在下次运行Effect
之前清理之前渲染的effect
;
参考如下代码理解useEffect
的运行与清理:
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.id, handleStatussChange);
return() => {
ChatAPI.unsubscribeToFriendStatus(props.id, handleStatusChange);
};
});
假如第一次渲染时props是{id: 10}
,第二次渲染时是{id: 20}
,你可能认为:
{id: 10}
的effect;{id: 20}
的UI;{id: 20}
的effect而实际上,如上所说,useEffect
会在下次运行Effect
之前清理之前渲染的effect
,其执行过程如下:
{id: 20}
的UI;{id: 10}
的effect;{id: 20}
的effectuseEffect
中的函数依赖处理(参考A Complete Guide to useEffect)
effect
中调用,那么可以吧函数直接定义在effect
内部。这样我们不需要再去关心那些间接依赖;effect
中——比如函数是从props
中获取的,或者函数逻辑在多个effect
中可以复用:
effect
中使用;另外,你不需要将其设置为依赖,因为其不在渲染范围内,不引用props
和state
,不受数据流影响;function getFetch(query) {
return 'http://caniuse.com?query=' + query;
}
function SearchResults() {
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, []); // ✅ Deps are OK
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, []); // ✅ Deps are OK
// ...
}
useCallback
将函数进行包裹——**useCallback
的本质是添加了一层依赖检查,使得函数本身只在需要的时候才改变,而不是去掉对函数的依赖。**这样可以避免在渲染范围内的函数每次都执行。function SearchResults() {
// ✅ Preserves identity when its own deps are the same
const getFetchUrl = useCallback((query) => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, []); // ✅ Callback deps are OK
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK
// ...
}
参考文献