现在编写新的组件的时候我们有两个选择:class组件和函数组件。那么什么是函数组件呢?
现在来看一段函数:
function Welcome(props) {
return Hello, { props.name }
;
}
可以看到,函数组件是一个返回值是 DOM 结构的函数。
函数组件与 class 组件的区别
函数式组件没有组件实例化的过程,无实例化过程也就不需要分配多余的内存,从而性能得到一定的提升;
函数组件本身没有 this 的,所以使用 Ref 等模块时与类组件也有所区别;
函数式组件本身没有内部的 state,数据依赖于 props 的传入,所以又称为无状态组件;
函数式组件本身是没有访问生命周期的方法。
如何使用函数式组件,又可以使用 React 的强大功能呢?从 React 16.8 开始,React 新增 Hook 特性,使得可以在不编写 class 的情况下使用 state 以及其他的 React 特性。这使得函数式组件也可以实现 class 类组件的功能。现在,我们终于可以不用写类,也可以很方便地编写复杂的功能了。
何时使用函数式组件
函数式组件相比类组件,拥有更好的性能和更简单的职责,十分适合分割原本很宠大的组件。
什么是 Hook?
Hook 是可以在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用,应在函数式组件中使用,使得不使用 class 也能使用 React。
Hook 使用规则
只在最顶层使用 Hook
只在函数最外层调用 Hook,不要在循环,条件或嵌套函数中调用 Hook,确保总是在 React 函数的最顶层调用。这样就能确保 Hook 在每次渲染中按照同样的顺序被调用。
只在React 函数中调用 Hook
不要在普通的函数中调用 Hook。可以在:
在 React 函数组件中调用 Hook
在自定义 Hook 中调用
遵循此规则,可以确保组件的状态逻辑清晰。
下面来具体介绍一下 Hook:
useState
const [state, setState] = useState(initialState);
useState 是允许在 React 函数组件中添加 state 的 Hook。React 会在重复渲染时保留这个 State。useState 唯一的参数是初始 State,这个初始 State 参数只在第一次渲染时会被用到。useState 返回一对值:当前状态和更新 state 的函数。
在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。
首先看一下state hook:
import React, { useState } from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量,初始值为0
const [count, setCount] = useState(0);
return (
You clicked {count} times
);
}
再看一下等价的class:
import React from 'react';
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
You clicked {this.state.count} times
);
}
}
这两段代码功能是一样的,每单击一次按钮将 count + 1,但是函数组件相比 class 组件更简单更直观。
惰性初始 state
const [count, setCount] = useState(() => {
const initialState = handleComputation(props);
return initialState;
});
如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用。
函数式更新 state
import React, { useState } from 'react';
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: { count }
>
);
}
新的 state 需要通过使用先前的 state 计算得出,可以将函数传递给 setCount,该函数将接收先前的 state,并返回一个更新后的值。
useEffect
useEffect(didUpdate, [deps]);
useEffect 给函数组件增加了操作副作用的能力,也可看作是 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。第二个参数用来控制如何渲染,当第二个参数存在时,只有当该值改变时才会触发渲染。
默认情况下,effect 将在每轮渲染结束后执行,加上第二个参数可以让它在只有某些值改变的时候才执行。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 与 componentDidMount 和 componentDidUpdate 作用相同
useEffect(() => {
// 更新 document 的 title
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count更改时更新
return (
You clicked {count} times
);
}
每次渲染都执行 effect 可能会导致性能问题,在class组件中可以通过 componentDidUpdate 中添加 prevProps 或 prevState 的比较逻辑解决;在 useEffect 中可以通过第二个可选参数来控制渲染,仅当 count 改变的时候才会执行 effect,否则会跳过这个 effect,实现性能的优化。
相同的功能使用 class 再来实现一下:
import React from 'react';
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 (
You clicked {this.state.count} times
);
}
}
在以上的示例中,需要在两个生命周期函数中编写相同的代码,但是 React 的 class 组件并没有提供这样的方法。
需要清除的 effect
实际项目当中,有一些副作用是需要清除的。例如订阅外部数据源,定时器等。这种情况下,清除工作是非常重要的,可以防止引起内存泄露。
首先看一下 class 组件:
import React from 'react';
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...
可以看到:设置 document.title 的逻辑被分割到 componentDidMount 和 componentDidUpdate 中,订阅逻辑被分割到 componentDidMount 和 componentWillUnmount 中,componentDidMount 同时包含了两个不同的功能。
现在再来看看 effect 的实现:
import React, { useState, useEffect } from 'react';
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props]);
// ...
}
每个 effect 都可以返回一个清除函数,可以将添加和移除订阅的逻辑放在一起。在每次渲染的时候 React 会对前一个 effect 进行清除再执行当前的 effect。
Hook 允许我们使用多个 Effect 实现关注点分离。React 将按照 effect 声明的顺序依次调用组件中的 effect。
注意: 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([ ])作为第二个参数,说明 effect 不依赖于 props 或 state 中的任何值,它也不需要重复执行。
useContext
const value = useContext(MyContext);
接收一个 context 对象(React.CreateContext 的返回值)并返回该 context 的当前值。当组件上层最近的 MyContext.Provider 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先组件使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
调用了 useContext 的组件总会在 context 值变化时重新渲染。
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
);
}
function Toolbar(props) {
return (
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
);
}
useCallback
useCallback(function, [deps])
useCallback 的作用是返回一个记忆化的回调函数。第一个参数是一个回调函数,第二个参数是依赖项数组,在依赖项改变时会触发更新。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的记忆版本,该回调函数仅在某个依赖项改变时才会更新。
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
接收 (state, action) => newState 的 reducer,返回当前的 state 以及配套的 dispatch 方法。适用于 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等的场景。
指定初始的 state -- 将初始 state 作为第二个参数传入
const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
);
惰性初始化 -- 将 init 函数作为第三个参数传入
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}
>
);
}
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo 的作用是返回一个记忆化的值。传入 useMemo 的函数会在渲染期间执行,不要在函数内部执行与渲染无关的操作,注意:副作用的操作属于 useEffect 的范畴。
如果没有提供依赖项数组,useMemo 会在每次渲染时计算新的值,只有将依赖项数组作为参数传给 useMemo,它会在某个依赖项改变时才重新计算值,这有助于避免每次渲染时都进行高开销的计算。
useLayoutEffect
useLayoutEffect(didUpdate, [deps]);
在所有的 DOM 变更之后同步调用 effect。使用它读取 DOM 布局并同步触发重渲染。在浏览器执行绘制前, useLayoutEffect 内部的DOM 变更同步执行,才不会感觉到视觉上的不一致。
注意:尽可能使用标准的 useEffect 以避免阻塞视觉更新。
useRef
const refContainer = useRef(initialValue);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue),返回的 ref 对象在组件的整个生命周期内保持不变。它可以很方便地保存任何可变值,会在每次渲染时返回同一个 ref 对象。
注意:ref 对象内容发生变化时,useRef 不会通知,变更 .current 属性不会引发组件重新渲染。想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,应使用回调 ref 实现。
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
>
);
}
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle 可以在使用 ref 时自定义暴露给父组件的实例值,应与 forwardRef 一起使用。
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return ;
}
FancyInput = forwardRef(FancyInput);
父组件使用 inputRef.current.focus() 来调用。
useDebugValue
useDebugValue(value, formatFunction)
useDebugValue 用于在 React 开发者工具里显示自定义 hook 的标签。
格式化 debug 值
useDebugValue(date, date => date.toDateString());
useDebugValue 接受格式化函数作为可选的第二个参数。该函数接受 debug 值作为参数,并会返回一个格式化的显示值。
自定义 Hook
自定义 Hook 是一个函数,可以将组件逻辑提取到可重用的函数。名称以" use "开头,函数内部可以调用其他的 Hook。
现在来定义一个 useFriendStatus Hook:
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
}, [friendID]);
return isOnline;
}
这里 useFriendStatus 作用是订阅好友的在线状态。
将 useFriendStatus 在不同的组件中使用:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
{props.friend.name}
);
}
现在很好地实现了好友在线状态的复用,只需 const isOnline = useFriendStatus(props.friend.id);
就可知道好友的状态了。
自定义 Hook 是一种重用状态逻辑的机制,每次使用自定义 Hook 时,其中所有的 state 和副作用都是完全隔离的。