组件
React.Component
React.Component
是使用 ES6 classes 方式定义 React 组件的基类:
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
React.PureComponent
React.PureComponent
与 React.Component
两者的区别在于 React.Component
并未实现 shouldComponentUpdate()
,
而 React.PureComponent
中以浅层对比 prop 和 state 的方式来实现了该函数。
如果赋予 React 组件相同的 props 和 state,render()
函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent
可提高性能。
注意:
React.PureComponent
中的 shouldComponentUpdate()
仅作对象的浅层比较。如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。
仅在你的 props 和 state 较为简单时,或者在深层数据结构发生变化时调用 forceUpdate()
来确保组件被正确地更新时才使用 React.PureComponent
。
React.PureComponent
中的 shouldComponentUpdate()
将跳过所有子组件树的 prop 更新。因此,请确保所有子组件也都是“纯”的组件。
React.memo
const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
});
React.memo
为高阶组件。
React.memo
仅检查 props 变更。
如果函数组件被 React.memo
包裹,且其实现中拥有 useState
或 useContext
的 Hook,当 context 发生变化时,它仍会重新渲染。
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
export default React.memo(MyComponent, areEqual);
注意:
与 class 组件中 shouldComponentUpdate()
方法不同的是,如果 props 相等,areEqual
会返回 true
;如果 props 不相等,则返回 false
。这与 shouldComponentUpdate
方法的返回值相反。
转换元素
cloneElement():以 element
元素为样板克隆并返回新的 React 元素。
返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。
新的子元素将取代现有的子元素,而来自原始元素的 key
和 ref
将被保留。
React.cloneElement(
element,
[props],
[...children]
)
React.cloneElement()
几乎等同于:
<element.type {...element.props} {...props}>{children}</element.type>
isValidElement():
验证对象是否为 React 元素.
React.Children
提供了用于处理 this.props.children
不透明数据结构的实用方法。
React.Children.map
React.Children.map(children, function[(thisArg)])
在 children
里的每个直接子节点上调用一个函数,并将 this
设置为 thisArg
。
如果 children
是一个数组,遍历后还会为数组中的每个子节点调用该函数。
如果子节点为 null
或是 undefined
,则此方法将返回 null
或是 undefined
,
注意:
如果 children
是一个 Fragment
对象,它将被视为单一子节点的情况处理,而不会被遍历。
React.Children.forEach
React.Children.forEach(children, function[(thisArg)])
React.Children.count
返回 children
中的组件总数量,等同于通过 map
或 forEach
调用回调函数的次数。
React.Children.only
验证 children
是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误。
注意:
React.Children.only()
不接受 React.Children.map()
的返回值,因为它是一个数组而并不是 React 元素。
React.Children.toArray
将 children
这个复杂的数据结构以数组的方式扁平展开并返回,并为每个子节点分配一个 key。
当你想要在渲染函数中操作子节点的集合时,它会非常实用,
特别是当你想要在向下传递 this.props.children
之前对内容重新排序或获取子集时。
注意:
React.Children.toArray()
在拉平展开子节点列表时,更改 key 值以保留嵌套数组的语义。也就是说,toArray
会为返回数组中的每个 key 添加前缀,以使得每个元素 key 的范围都限定在此函数入参数组的对象内。
Ref
createRef():创建一个能够通过 ref 属性附加到 React 元素的 ref。
React.forwardRef()
React.forwardRef
会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。
React.forwardRef
接受渲染函数作为参数。React 将使用 props
和 ref
作为参数来调用此函数。此函数应返回 React 节点。
用途:
- 转发refs到dom组件
- 在高阶组建中转发refs
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>
Click me!
</FancyButton>;
在上述的示例中,React 会将 <FancyButton ref={ref}>
元素的 ref
作为第二个参数传递给 React.forwardRef
函数中的渲染函数。该渲染函数会将 ref
传递给 <button ref={ref}>
元素。
因此,当 React 附加了 ref 属性之后,ref.current
将直接指向 <button>
DOM 元素实例。
Suspense
Suspense 使得组件可以“等待”某些操作结束后,再进行渲染。
目前,Suspense 仅支持的使用场景是:通过 React.lazy
动态加载组件。它将在未来支持其它使用场景,如数据获取等。
React.lazy
// 这个组件是动态加载的
const SomeComponent = React.lazy(() => import('./SomeComponent'));
注意
使用 React.lazy
的动态引入特性需要 JS 环境支持 Promise。在 IE11 及以下版本的浏览器中需要通过引入 polyfill 来使用该特性。
React.Suspense
React.Suspense
可以指定加载指示器(loading indicator),以防其组件树中的某些子组件尚未具备渲染条件。
// 该组件是动态加载的
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
// 显示 <Spinner> 组件直至 OtherComponent 加载完成
<React.Suspense fallback={<Spinner />}>
<div>
<OtherComponent />
</div>
</React.Suspense>
);
}
注意:
React.lazy()
和 <React.Suspense>
尚未在 ReactDOMServer
中支持。这是已知问题,将会在未来解决。
React.component
组件的生命周期
Mounting阶段:
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
Updating阶段:
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
Unmounting阶段:
错误处理:
当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:
constructor()
如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
如果不在其他语句之前调用 super(props),
那么this.props
在构造函数中可能会出现未定义的 bug。
在 React 中,构造函数仅用于以下两种情况:
static getDerivedStateFormProps(props,state)
getDerivedStateFromProps
会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。
它应返回一个对象来更新 state,如果返回 null
则不更新任何内容。
Render()
render()
函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。
注意
如果 shouldComponentUpdate()
返回 false,则不会调用 render()
。
componentDidMount()
componentDidMount()
会在组件挂载后(插入 DOM 树中)立即调用。
依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。
这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在 componentWillUnmount()
里取消订阅
可以在 componentDidMount()
里直接调用 setState()
。它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前,但会导致性能问题。
shouldComponentUpdate(nextProps,nextState)
默认行为是 state 每次发生变化组件都会重新渲染。
当 props 或 state 发生变化时,shouldComponentUpdate()
会在渲染执行之前被调用。返回值默认为 true。
首次渲染或使用 forceUpdate()
时不会调用该方法。
如果你一定要手动编写此函数,可以将 this.props
与 nextProps
以及 this.state
与nextState
进行比较,并返回 false
以告知 React 可以跳过更新。请注意,返回 false
并不会阻止子组件在 state 更改时重新渲染。
如果 shouldComponentUpdate()
返回 false
,则不会调用 UNSAFE_componentWillUpdate()
,render()
和 componentDidUpdate()
。
且当返回 false
时,仍可能导致组件重新渲染。
getSnapShotBeforeUpdate(preProps,preState)
getSnapshotBeforeUpdate()
在最近一次渲染输出(提交到 DOM 节点)之前调用。
能在组件更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()
。
此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。应返回 snapshot 的值(或 null
)
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否在 list 中添加新的 items ?
// 捕获滚动位置以便我们稍后调整滚动位置。
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
// 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}
在上述示例中,重点是从 getSnapshotBeforeUpdate
读取 scrollHeight
属性,因为 “render” 阶段生命周期(如 render
)和 “commit” 阶段生命周期(如 getSnapshotBeforeUpdate
和 componentDidUpdate
)之间可能存在延迟。
componentDidUpdate(preProps,preState,snapshot)
componentDidUpdate()
会在更新后会被立即调用。首次渲染不会执行此方法。
当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。
可以在 componentDidUpdate()
中直接调用 setState()
,但请注意它必须被包裹在一个条件语句里
componentDidUpdate(prevProps) {
// 典型用法(不要忘记比较 props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
为什么props传给state会产生bug?
一个组件会接收多个 prop,任何一个 prop 的改变都会导致重新渲染和不正确的状态重置,数据就可能会丢失。
componentWillUnmount()
会在组件卸载及销毁之前直接调用。
可以在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount()
中创建的订阅等。
componentWillUnmount()
中不应调用 setState()
,因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。
错误处理
static getDerivedStateFromError(error)
此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false
};
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以显降级 UI
return { hasError: true };
}
render() {
if (this.state.hasError) {
// 你可以渲染任何自定义的降级 UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
componentDidCatch(error,info)
componentDidCatch()
会在“提交”阶段被调用,因此允许执行副作用。 它应该用于记录错误之类的情况:
其他API
setState()
将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。
React会批量延迟调用setState(),然后通过一次传递更新多个组件,所以无法在调用setState()后直接取state值。
componentDidUpdate
或者 setState
的回调函数(setState(updater, callback)
),这两种方式都可以保证在应用更新后触发。
forceUpdate()
可以调用 forceUpdate()
强制让组件重新渲染。
调用 forceUpdate()
会使组件调用 render()
方法,此操作会跳过该组件的 shouldComponentUpdate()
。
但其子组件会触发正常的生命周期方法,包括 shouldComponentUpdate()
方法。如果标记发生变化,React 仍将只更新 DOM。不推荐
class属性
defaultProps
defaultProps可以为class组件添加默认props,一般用于props未赋值但又不能为null的情况。
class CustomButton extends React.Component {
// ...
}
CustomButton.defaultProps = {
color: 'blue'
};
Hook
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
为什么用Hook?
1.在组件之间复用状态逻辑很难。可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。
2.复杂组件变得难以理解。产生一些bug。Hook 将组件中相互关联的部分拆分成更小的函数,比如设置订阅或请求数据
3.难理解的class。比如class 不能很好的压缩、会使热重载出现不稳定的情况。因此,我们想提供一个使代码更易于优化的 API。Hook 使你在非 class 的情况下可以使用更多的 React 特性。
state Hook
在这里useState就是一个Hook,
useState
会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。
它类似 class 组件的 this.setState
,但是它不会把新的 state 和旧的 state 进行合并。
import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量。
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Hook是什么?
Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState
是允许你在 React 函数组件中添加 state 的 Hook
Hook 不能在 class 组件中使用,
Hook 使用了 JavaScript 的闭包机制。
什么时候用Hook?
如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其转化为 class。现在你可以在现有的函数组件中使用 Hook。
useState和this.state提供的功能完全相同。
useState()唯一的参数是初始state
(如果我们想要在 state 中存储两个不同的变量,只需调用 useState()
两次即可。)
一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
const [count, setCount] = useState(0);
这是数组解构。它意味着我们同时创建了 count和 setCount
两个变量,count
的值为 useState
返回的第一个值,setCount
是返回的第二个值。
它等价于下面的代码:
var countStateVariable = useState(0); // 返回一个有两个元素的数组
var count = countStateVariable[0]; // 数组里的第一个值
var setCount = countStateVariable[1]; // 数组里的第二个值
class和Hook中state(useState和this.state)的区别:
1.this.state的赋值一定是对象,useState的赋值可以是数字、字符串、对象、数组
2.前者读取state是这样的写法:{this.state.xxx};后者可以直接{xxx}
3.前者需要通过this.setState()更新,后者setXXX(XXX+1)
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
使用多个state变量
function ExampleWithManyStates() {
// 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
在以上组件中,我们有局部变量 age
,fruit
和 todos
,并且我们可以单独更新它们:
function handleOrangeClick() {
// 和 this.setState({ fruit: 'orange' }) 类似
setFruit('orange');
}
Effect Hook
在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”.
useEffect
就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
具有相同的用途,只不过被合并成了一个 API。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 相当于 componentDidMount 和 componentDidUpdate:
useEffect(() => { // 使用浏览器的 API 更新页面标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Effect Hook分为需要清除的和不需要清除的:
无需清除的Effect
即在 React 更新 DOM 之后运行一些额外的代码。
比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。
因为我们在执行完这些操作之后,就可以忽略他们了。
class和Hook“副作用”区别?
1.class中副作用操作放到 componentDidMount
和 componentDidUpdate
函数中。
2.如果想让组件在加载和更新时执行同样的操作,class中只能在2个生命周期函数中写重复的代码。
Hook中只用写一次,useEffect()默认情况下载第一次渲染后和每次更新后都会执行。
3.使用 useEffect
调度的 effect 不会阻塞浏览器更新屏幕。class会。
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
需要清除的Effect
class:
在 React class 中,你通常会在 componentDidMount
中设置订阅,并在 componentWillUnmount
中清除它。例如,假设我们有一个 ChatAPI
模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
Hook:
如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:
为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
React什么时候执行清除effect?
在组件卸载的时候执行清除操作。
Hook的使用规则
Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
- 只能在函数最顶层调用 Hook。不要在循环、条件判断或者子函数中调用。【目的:确保 Hook 在每一次渲染中都按照同样的顺序被调用。】
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中,我们稍后会学习到。)
Hook可以定义多个state变量,React怎么知道哪个state对于哪个state?
React 靠的是 Hook 调用的顺序。
只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。
如果在条件、循环、嵌套中调用Hook,第一次可能没事,第二次渲染会跳过该 Hook,Hook 的调用顺序发生了改变。
function Form() {
// 1. Use the name state variable
const [name, setName] = useState('Mary');
// 2. Use an effect for persisting the form
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
// 3. Use the surname state variable
const [surname, setSurname] = useState('Poppins');
// 4. Use an effect for updating the title
useEffect(function updateTitle() {
document.title = name + ' ' + surname;
});
// ...
}
// ------------
// 首次渲染
// ------------
useState('Mary') // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm) // 2. 添加 effect 以保存 form 操作
useState('Poppins') // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle) // 4. 添加 effect 以更新标题
// -------------
// 二次渲染
// -------------
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm) // 2. 替换保存 form 的 effect
useState('Poppins') // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle) // 4. 替换更新标题的 effect
// ...
自定义Hook
自定义 Hook 可以让你在不增加组件的情况下在组件之间重用一些状态逻辑。
Hook 是一种复用状态逻辑的方式,它不复用 state 本身。
Hook 的每次调用都有一个完全独立的 state —— 因此你可以在单个组件中多次调用同一个自定义 Hook。
如果函数的名字以 “use
” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。原因是:可以帮助 linter 插件在使用 Hook 的代码中找到 bug。
通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。
自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。
Hook API
基础Hook:
useState
函数式更新:
如果新的state需要通过之前的state算出来,可以通过将函数传给setState。setState接收之前的State然后返回更新后的值。
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。
注意
与 class 组件中的 setState
方法不同,useState
不会自动合并更新对象。你可以用函数式的 setState
结合展开运算符来达到合并更新对象的效果。
setState(prevState => {
// 也可以使用 Object.assign
return {...prevState, ...updatedValues};
});
useReducer
是另一种可选方案,它更适合用于管理包含多个子值的 state 对象。
惰性初始 state
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
useEffect
赋值给 useEffect
的函数会在组件渲染到屏幕之后执行。
默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 (条件effect)才执行。
清除effect
为防止内存泄漏,清除函数会在组件卸载前执行。
如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。
effect的执行时机
useEffect
会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。
但并非所有 effect 都可以被延迟执行。
例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect
Hook 来处理这类 effect。
effect的条件执行
默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。
在某些场景下这么做可能会矫枉过正。所以有了effect的第二个参数
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
只有当 props.source
改变后才会重新创建。
注意
如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([]
)作为第二个参数,但effect 内部的 props 和 state 就会一直持有其初始值。
推荐启用 eslint-plugin-react-hooks
中的 exhaustive-deps
规则。此规则会在添加错误依赖时发出警告并给出修复建议。
useContext
const value = useContext(MyContext);
接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value
prop 决定。
当组件上层最近的 <MyContext.Provider>
更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext
provider 的 context value
值。
即使祖先使用 React.memo
或 shouldComponentUpdate
,也会在组件本身使用 useContext
时重新渲染。
useContext
的参数必须是 context 对象本身
调用了 useContext
的组件总会在 context 值变化时重新渲染,如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化。
提示:
useContext(MyContext)
相当于 class 组件中的 static contextType = MyContext
或者 <MyContext.Consumer>
。
useContext(MyContext)
只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider>
来为下层组件提供 context。
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
额外Hook:
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
在某些场景下,useReducer
会比 useState
更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer
还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch
而不是回调函数 。
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();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
指定初始state
将初始 state 作为第二个参数传入 useReducer
:
const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
);
function init(initialCount) { return {count: initialCount};}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset': return init(action.payload); default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个 memoized 值.
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
传入 useMemo
的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect
的适用范畴,而不是 useMemo
。
如果没有提供依赖项数组,useMemo
在每次渲染时都会计算新的值。
useRef
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内保持不变。
本质上,useRef
就像是可以在其 .current
属性中保存一个可变值的“盒子”。
如果你将 ref 对象以 <div ref={myRef} />
形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current
属性设置为相应的 DOM 节点。
useRef()
比 ref
属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。因为它创建的是一个普通 Javascript 对象。而 useRef()
和自建一个 {current: ...}
对象的唯一区别是,useRef
会在每次渲染时返回同一个 ref 对象。
当 ref 对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle
应当与 forwardRef
一起使用:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
useLayoutEffect
其函数签名与 useEffect
相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect
内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect
以避免阻塞视觉更新。
注意:无论 useLayoutEffect
还是 useEffect
都无法在 Javascript 代码加载完成之前执行。
useDebugValue(value)
useDebugValue
可用于在 React 开发者工具中显示自定义 hook 的标签。
Hooks FAQ
哪个版本的React包含Hook?
从16.8.0开始,React 在以下模块中包含了 React Hook 的稳定实现:
- React DOM
- React Native
- React DOM Server
- React Test Renderer
- React Shallow Renderer
要启用 Hook,所有 React 相关的 package 都必须升级到 16.8.0 或更高版本。如果你忘记更新诸如 React DOM 之类的 package,Hook 将无法运行。
有什么是 Hook 能做而 class 做不到的?
自定义Hook,且让函数组件有了state
Hook 能否覆盖 class 的所有使用场景?
目前暂时还没有对应不常用的 getSnapshotBeforeUpdate
,getDerivedStateFromError
和 componentDidCatch
生命周期的 Hook 等价写法,但我们计划尽早把它们加进来。
目前 Hook 还处于早期阶段,一些第三方的库可能还暂时无法兼容 Hook。
Hook 对于 Redux connect()
和 React Router 等流行的 API 来说,意味着什么?
你可以继续使用之前使用的 API;它们仍会继续有效。
React Redux 从 v7.1.0 开始支持 Hook API 并暴露了 useDispatch
和 useSelector
等 hook。
React Router 从 v5.1 开始支持 hook。
其它第三库也将即将支持 hook。
Hook 能和静态类型一起用吗?
Hook 在设计阶段就考虑了静态类型的问题。因为它们是函数,所以它们比像高阶组件这样的模式更易于设定正确的类型。最新版的 Flow 和 TypeScript React 定义已经包含了对 React Hook 的支持。
从 Class 迁移到 Hook,生命周期方法要如何对应到 Hook?
constructor
:函数组件不需要构造函数。你可以通过调用useState
来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给useState
。getDerivedStateFromProps
:改为 在渲染时 安排一次更新。shouldComponentUpdate
:详见 下方React.memo
.render
:这是函数组件体本身。componentDidMount
,componentDidUpdate
,componentWillUnmount
:useEffect
Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。getSnapshotBeforeUpdate
,componentDidCatch
以及getDerivedStateFromError
:目前还没有这些方法的 Hook 等价写法,但很快会被添加。
如何获取上一轮的 props 或 state?
目前,通过ref来实现。
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return <h1>Now: {count}, before: {prevCount}</h1>;
}
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
如果前后两次的值相同,useState
和 useReducer
Hook 都会放弃更新。原地修改 state 并调用 setState
不会引起重新渲染。
什么情况下能把函数从依赖列表中省略?
只有 当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
// 使用了 productId prop
const json = await response.json();
setProduct(json);
}
useEffect(() => {
fetchProduct();
}, []); // 🔴 这样是无效的,因为 `fetchProduct` 使用了 `productId` // ...
}
推荐的修复方案是把那个函数移动到你的 effect 内部。
如何避免向下传递回调?
我们推荐的替代方案是通过 context 用 useReducer
往下传一个 dispatch
函数:
const TodosDispatch = React.createContext(null);
function TodosApp() {
// 提示:`dispatch` 不会在重新渲染之间变化 const [todos, dispatch] = useReducer(todosReducer);
return (
<TodosDispatch.Provider value={dispatch}>
<DeepTree todos={todos} />
</TodosDispatch.Provider>
);
}
React 是如何把对 Hook 的调用和组件联系起来的?
每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState()
调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState()
调用会得到各自独立的本地 state 的原因。