本篇将重点介绍 Hook API 的具体使用方式。之后的文章会介绍原理。
useState
我们首先使用Hook来声明一个state,这是不管做什么都需要的一个API。
import React, { useState } from 'react';
function Example() {
// 声明一个叫'count' 的 state
const [count, setCount] = useState(0)
}
调用 useState 方法的时候都做了什么?
帮我们定义了一个state变量,这个变量叫做count,可以叫做任何名字。一般来说,在函数推出之后变量就会“消失”,而state中的变量会被React保留。
useState 需要哪些参数?
只需要一个参数,这个参数将会是count的默认值。我们可以传数字、字符串、对象等,这完全看你个人的需求。如果你想创建两个变量,再多调用一次useState。
useState 的返回值是什么?
返回的是当前的state,以及修改state的函数。setCount这个函数将只会修改count,所以要特别注意,避免命名冲突。
如何读取/更新state?
我们想在DOM中使用我们的state,展示到页面上:
import React, { useState } from 'react';
function Example() {
// 声明一个叫'count' 的 state
const [count, setCount] = useState(0)
return (
<div>这是 conunt:{ count }</div>
)
}
我们想要更新页面的state,从而达到数据响应式:
import React, { useState } from 'react';
function Example() {
// 声明一个叫'count' 的 state
const [count, setCount] = useState(0)
const fn = () => {
setCount(count + 1)
}
return (
<div>这是 conunt:{ count }</div>
<button onClick={() => fn()}></button>
)
}
useEffect
在react组件中有两种常见的副作用操作,需要清除的和不需要清除的。
无需清除的 effect
有时候,我们只想在 React 更新DOM之后运行一些额外的代码。比如发送网络请求,记录日志等,这些都是常见的不需要请求的操作。因为我们在执行完这些操作之后,就可以忽略他们了。
看看下面这段代码:
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>
);
}
useEffect 做了什么?
这个Hook可以做到,在渲染结束之后执行哪些操作。
React内部会保存你传递的函数,在DOM更新之后调用这个函数(这个函数称为effect)。
为什么在组件内部调用uesEffect?
放在组件内能够直接访问count变量,或者其他的props。
Hook 使用了 JavaScript 的闭包机制。
useEffect 会在每次渲染之后都执行么?
默认情况下是这样的,React 保证了在每次运行 effect 函数的同时,DOM都是更新完毕的。
他也是可以进行控制的。
注意
- 每次重新渲染,都会生成新的 effect,替换掉之前的 effect。
- 与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 不会阻塞浏览器更新屏幕。
需要清除的 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';
}
如果你的effect返回一个函数,React会在执行清除操作时调用这个函数。
为什么要在 effect 中返回一个函数?
设计的想法是:这两部分代码都作用于相同的副作用,它们都属于effect的一个部分,而不是将他们进行拆分。
React 何时清除 effect?
他会在组件卸载的时候执行清除操作。
他是在调用新的effect之前对之前的effect进行清除。比如,第一个effect被调用,而清除的effect将会被存储,等第二次调用effect的时候,先回清除第一个effect。
通过跳过 Effect 进行性能优化
在某些情况下,每次渲染之后都要执行effect,可能会导致性能问题。
如果某些特定的值在两次重新渲染之间没有发生变化,就可以跳过对effect函数的执行。如下面的例子:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
第二个参数就是为了如此,如果[count]中的count没有发生改变,就不会执行effect。
如果只想执行一次effect,就传递一个空数组。
useContext
这个Hook需要配合其他的方法使用,举个例子来看:
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>
);
}
首先由 React.createContext 返回一个 context 对象。
我们的组件被ThemeContext.Provider包裹,例如:<ThemeContext.Provider value={themes.dark}>我们的组件</ThemeContext.Provider>
,这样value会被层层传递下去。哪怕再深的子组件都会接收到value传递的值。
深层的子组件可以通过,useContext(ThemeContext)
方式来接收外层传递进来的value,useContext会返回这个value。
优点
- 不会再使用props进行传递了。
- 不会出现props那样子逐层传递过深了。
- 状态管理变得更轻松。哪怕是兄弟组件,可以通过对象的方法进行修改同一个状态。
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
三个参数:
- reducer: 执行函数。
- initialArg: 默认值。
- 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}
<button onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
Counter这个函数和将会接受一个参数,这个参数会通过useReducer传递给init函数,然后会作为state的默认值。
当调用dispatch时,传递一个参数,这个参数作为reducer的action,而init函数的返回值,作为reducer的state。
会不会觉得很绕?看一下这个例子:codesandbox.io/s/relaxed-b…
跳过 dispatch
如果 Reducer Hook 的返回值与当前state相同,React 将跳过子组件的渲染以及副作用的执行。
React 内部使用的是 Object.is比较算法 来比较state的。
useCallback
useCallback的两个参数:
-
函数,当监听的第二个参数发生改变时被调用
-
数组,监听数组内的值是否发生变化,如果是空数组将只执行一次。 useCallback的返回值:
-
返回给我们一个函数。 看下面这个例子
import "./styles.css";
import React, { useCallback, useState } from "react";
export default function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const memoizedCallback = useCallback(() => {
console.log(a, b);
setA(a + 1);
setB(b + 1);
}, [a, b]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{a}----{b}
<button onClick={() => memoizedCallback()}>click</button>
</div>
)
}
当点击click按钮时,调用useCallback返回的函数memoizedCallback,会对useCallback第一个参数的函数进行调用。
useMemo
useMemo的两个参数:
- 函数,当依赖项数组发生改变之后调用
- 依赖项数组,如果没有提供,每次都会渲染时都会计算新的值。
useMemo返回值:
- 返回的是value值,这与useCallback不同。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
仅仅会在某个依赖项改变的时候进行重新计算。这样会避免每次渲染时都会进行高开销的计算。
useRef
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
useRef会返回一个可变的Ref对象,.current 属性被初始化为传入的参数。
当 .current 属性发生改变,并不会引发组件的重新渲染。
useLayoutEffect
函数签名与useEffect相同,淡会在你所有的DOM变更之后同步调用 effect。
可以使用它来读取DOM布局,并同步触发重新渲染。
useLayoutEffect是在浏览器绘制执行之前,内部的更新计划会被同步刷新。
useImperativeHandle
它的作用是让子组件在使用ref
的时候,可以控制哪些值不需要暴露给父组件。
他因该与forwardRef一起使用。
看下面这个例子:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
FancyInput将会成为<FancyInput ref={inputRef}>
组件,而父组件只可以调用focus函数中暴露出去的inputRef.current.focus()
方法。
useDebugValue
他是在React开发工具中显示自定义Hook标签。
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
// 在开发者工具中的这个 Hook 旁边显示标签
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
可以接受一个格式化函数作为第二个参数,这个函数只有Hook被检查的时候才会被调用。他接受debug的值作为参数,并且会返回一个格式化的显示值。
useDebugValue(date, date => date.toDateString());
比如上面的这个例子,一个返回date值的自定义Hook可以通过格式化函数来避免不必要的toDtaeString函数调用。
总结
-
基础 Hook
- useState
- useEffect
- useContext
-
额外的 Hook
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue