在之前的学习中,我们了解到只有类组件中才能使用 state,函数组件是无法使用的。但Hook的出现,改变这种情况,Hook是React 16.8 版本的新增特性,可以让我们在函数组件中使用state、生命周期以及refs、context等其他相关特性。而且Hook可以让我们在不更改组件结构的前提下复用状态逻辑,使得在组件间共享Hook变得十分便利。
简单来说,Hook就是一些React中封装好的state以及生命周期、refs等特性的方法,本质就是 JavaScript 函数。除了React为我们准备好的Hook,我们还可以自己创建自定义Hook。
现在React在企业中的开发模式通常为:Hook+函数组件的开发模式。
官网文档: https://zh-hans.reactjs.org/docs/hooks-intro.html
通常我们会把Hook写在组件内的最前面,这样才能确保 Hook 在每一次渲染中都按照同样的顺序被调用。让 React 能够在多次的 useState
和 useEffect
调用之间保持 hook 状态的正确,因为React是通过 Hook 的调用顺序来明确 state 和 useState 的对应关系。
State Hook 是用来创建 state 的Hook,可以通过在函数组件中调用 useState 方法来给组件设置 state。该方法只有一个参数,表示创建的 state 的初始值,这个初始值只有在第一次渲染的时候才会被用到。我们通过数组解构来获取方法的返回值,返回值有两个,第一个返回值为所创建的 state,第二个返回值为更新所创建的 state 的函数,类似于 setState,函数名为:set + state的名字(首字母大写)。想要更新创建的state,只能通过该更新函数来更新,函数的参数就是更新后变量的值。
// 利用数组解构 获取 count 变量 和更新函数setCount
// 并设置初始值为 0
const [count, setCount] = useState(0);
如果想要在一个函数组件中声明多个 state 变量,只需要多次调用 useState 方法即可,但 state 变量名不能重复。
// 1、导入 useState
import React, { useState } from 'react';
function Example() {
// 2、声明一个叫 "count" 的 state 变量 并声明初始值为 0
// 利用数组解构 获取 count 变量 和更新函数setCount
const [count, setCount] = useState(0);
// 声明多个变量
const [Uname, setUname] = useState('猪猪侠');
const [info, setInfo] = useState({age: 18,height: '180cm'});
return (
{/* 3、调用变量 */}
{You} clicked {count} times
{/* 4、调用更新函数 */}
);
}
class Example extends React.Component {
constructor(props) {
super(props);
// 声明并初始化 state
this.state = {
count: 0
};
}
render() {
return (
You clicked {this.state.count} times
);
}
在React官方文档中,把数据获取、操作DOM、订阅数据、调用浏览器API等操作,称为副作用(真tn抽象,鬼才翻译!)。而Effect Hook 就是通过 useEffect 来给函数组件提供操作这些副作用的能力的方法,作用相当于类组件的生命周期钩子函数(componentDidMount
、componentDidUpdate
和 componentWillUnmount
),只不过将这多个函数合并构成了一个Hook。而且与这些钩子函数不同,使用 useEffect
不会阻塞浏览器更新屏幕,属于异步执行,这让应用看起来响应更快。
// 使用 useEffect 修改页面标题
useEffect(() => {
document.title = `You clicked ${count} times`;
});
组件中可以多次调用 useEffect ,对不同的副作用进行操作,将不相关的逻辑分离到不同的effect中。React将按照顺序依次调用组件中的effect。
useEffect 有两个参数,第一个参数是一个回调函数,表示在React完成DOM更新渲染操作后再运行该函数,函数内对副作用进行操作,还可以访问到当前组件的 props 和 state。第二个参数是一个数组(可选),决定在何时触发 useEffect ,减少无效渲染,优化性能。只有当数组内元素的值发生变化后,才会触发对应的effect。默认情况下,每次页面 DOM 更新后都会触发 useEffect,包括页面的第一次渲染 。
如果只想在组件挂载和卸载时,运行 effect,只需要将 useEffect 的参数设置为 []
空数组即可。相当于告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。
// 只有 count 变量变化后 才会使用 useEffect 修改页面标题
useEffect(() => {
document.title = `You clicked ${count} times`;
},[count]);
// 只在挂载和卸载时 执行一次
useEffect(() => {
document.title = `You clicked times`;
},[]);
useEffect 还可以通过地那一个参数回调函数 return 返回一个函数,该函数表示如何清除设置的副作用,有些副作用是无需清除的,而有些副作用谁需要清除的,例如:事件解绑、取消订阅等。effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次,调用一个新的 effect 之前对前一个 effect 进行清理。每次我们重新渲染,都会生成新的 effect,替换掉之前的。
// 1、导入 Hook
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
// 2、创建变量
const [isOnline, setIsOnline] = useState(null);
·// 3、创建 effect
useEffect(() => {
// 在DOM渲染完之后 执行的代码
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// return 一个在销毁时执行的消除函数
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
// 只有当 props.friend.id 更新时才会触发
},[[props.friend.id]]);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
class FriendStatus extends React.Component {
// 1、创建变量
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
// 2、在不同的生命周期函数执行类似的代码 代码不简洁
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';
}
}
之前我们学习的React中组件之间共享状态逻辑的方法有两种:render props 和 高阶组件。现在我们也可以通过自定义Hook来实现函数组件间共享状态逻辑。
自定义 Hook 就是一个函数,但其名称必须以 “use
” 开头,而且函数内部可以调用其他的 Hook。我们不需要对共享的状态逻辑做改变,只是将两个函数之间一些共同的代码提取到单独的函数中。所以自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。
如果多个组件之间使用相同的自定义Hook,并不会共享 state 变量,只是复用了逻辑,每次使用自定义 Hook 时,其中的所有 state 和副作用操作都是完全隔离的。
如果我们在调用自定义Hook时,我们向Hook传递一个变量,Hook给我们返回一个值,那么每次变量更新后,React都会再次调用这个自定义Hook,获取最新的返回值。
自定义的Hook:
// 引入系统Hook
import { useState, useEffect } from 'react';
// 自定义Hook 接受一个参数
function useFriendStatus(friendID) {
// 创建变量
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
// 返回值
return isOnline;
}
在其他组件中调用:
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1);
// 使用自定义 Hook 每次recipientID变化 都会调用
const isRecipientOnline = useFriendStatus(recipientID);
return (...);
}
const value = useContext(MyContext);
useContext(MyContext)
相当于 class 组件中的 static contextType = MyContext
或者
。接收一个context对象为参数,并返回该context的当前值, 值由上层组件中距离当前组件最近的
的 value
prop 决定。当组件上层最近的
更新时,该 Hook 会触发重渲染,并返回最新值。调用了 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 (
);
}
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。它与useImperativeHandle
结合使用,可以在父组件中访问子组件通过useImperativeHandle
暴露的数据。本质上,useRef
就像是在其 .current
属性中保存一个可变值的“盒子”,而这个盒子的值,由对应 ref
的子组件决定,也可以对对应的DOM元素进行渲染。
useRef
会在每次渲染时返回同一个 ref 对象,返回的 ref 对象在组件的整个生命周期内持续存在。但是变更 .current
属性不会引发组件重新渲染。
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
>
);
}
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle
可以让你在使用 ref
时自定义暴露给父组件的实例值。
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return ;
}
该Hook与useEffect
功能相同,只不过useEffect
是异步的,而useLayoutEffect
会在所有的 DOM 变更之后同步调用 effect,会在一定程度上阻塞视觉刷新。可以使用它来读取 DOM 布局并同步触发重渲染。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useCallback
可以返回一个 memoized 回调函数,把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo
可以返回一个 memoized 值,把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo
在每次渲染时都会计算新的值。
传入 useMemo
的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState
的替代方案。它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的修改对应值的 dispatch
方法。在某些场景下,useReducer
会比 useState
更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer
还能给那些会触发深更新的组件做性能优化,因为[你可以向子组件传递 dispatch
而不是回调函数 。
useState
的第一个参数是对应renducer方法,第二个参数用来指定初始值,第三个参数用来惰性初始化初始值。第二个参数设置初始值时,只需要传入一个对象,对象中存储属性和值。第三个参数惰性初始化,则需要声明一个init函数,通过这个函数来设置初始值,这样初始 state 将被设置为 init(initialArg)
。
// init函数
function init(initialCount) {
return {count: initialCount};
}
// 惰性初始化
const [state, dispatch] = useReducer(reducer, initialCount, init);
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, {count: 0});
return (
<>
Count: {state.count}
>
);
}
useDebugValue(value)
useDebugValue
可用于在 React 开发者工具中显示自定义 hook 的标签。