Hooks 的出现
在 React 中, Hooks 就是把某个目标结果钩到某个可能会变化的数据源或者事件源上, 那么当被钩到的数据或事件发生变化时, 产生这个目标结果的代码会重新执行, 产生更新后的结果.
所有的 Hooks 的最终结果都是导致 UI 的变化.
Hooks 的最大好处: 逻辑复用
Hooks 的另一大好处: 有助于关注分离
参考:
useState 保存组件状态和使用生命周期
useState 是用来管理 state, 让函数组件具有维持状态的能力
在一个函数组件的多次渲染之间, 这个 state 是共享的
// 定义一个年龄的 state, 初始值是 42
const [age, setAge] = useState(42);
// 定义一个水果的 state, 初始值是 banana
const [fruit, setFruit] = useState("banana");
// 定一个一个数组 state, 初始值是包含一个 todo 的数组
const [todos, setTodos] = useState([{ text: "Learn Hooks" }]);
useState 是一个非常简单的 Hook, 它让你很方便地去创建一个状态, 并提供一个特定的方法 (比如 setAge) 来设置这个状态
state 中永远不要保存可以通过计算得到的值
useEffect
useEffect 用于执行一段副作用
副作用是指一段和当前执行结果无关的代码, 比如说要修改函数外部的某个变量, 要发起一个请求, 等等
在函数组件的当次执行过程中, useEffect 中代码的执行不影响渲染出来的 UI
useEffect 是每次组件 render 完后判断依赖并执行
useEffect(callback, dependencies);
callback: 要执行的函数
dependencies: 可选的依赖项数组
依赖项是可选的
-
没有依赖项, 则每次 render 后都会重新执行
useEffect(() => { // 每次 render 完一定执行 console.log("re-rendered"); }); -
有依赖项, 且依赖项不为空数组
import React, { useState, useEffect } from "react"; function BlogView({ id }) { // 设置一个本地 state 用于保存 blog 内容 const [blogContent, setBlogContent] = useState(null); useEffect(() => { // useEffect 的 callback 要避免直接的 async 函数, 需要封装一下 const doAsync = async () => { // 当 id 发生变化时, 将当前内容清楚以保持一致性 setBlogContent(null); // 发起请求获取数据 const res = await fetch(`/blog-content/${id}`); // 将获取的数据放入 state setBlogContent(await res.text()); }; doAsync(); }, [id]); // 使用 id 作为依赖项, 变化时则执行副作用 // 如果没有 blogContent 则认为是在 loading 状态 const isLoading = !blogContent; return <div>{isLoading ? "Loading..." : blogContent}</div>; } -
空数组作为依赖项, 则只在首次执行时触发
useEffect(() => { // 组件首次渲染时执行,等价于 class 组件中的 componentDidMount console.log("did mount"); }, []);
useEffect 还允许返回一个函数, 用于在组件销毁的时候做一些清理的操作, 机制就几乎等价于类组件中的 componentWillUnmount
// 设置一个 size 的 state 用于保存当前窗口尺寸
const [size, setSize] = useState({});
useEffect(() => {
// 窗口大小变化事件处理函数
const handler = () => {
setSize(getSize());
};
// 监听 resize 事件
window.addEventListener("resize", handler);
// 返回一个 callback 在组件销毁时调用
return () => {
// 移除 resize 事件
window.removeEventListener("resize", handler);
};
}, []);
总结:
useEffect(() => {})每次 render 后执行useEffect(() => {}, [])仅第一次 render 后执行useEffect(() => {}, [deps])第一次 render 后及依赖项发生变化后执行useEffect(() => { return () => {} }, [])组件 unmount 后执行
参考:
useCallback 缓存回调函数
useCallback(fn, deps);
fn: 定义的回调函数 deps: 依赖的变量数组
只有当某个依赖变量发生变化时, 才会重新声明 fn 这个回调函数
// 修改前
// 每次组件状态发生变化的时候, 函数组件都会重新执行一遍
// 在每次执行的时候, 都会创建一个新的事件处理函数 handleIncrement
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => setCount(count + 1);
// ...
return <button onClick={handleIncrement}>+</button>;
}
// 修改后
// 只有 count 发生变化的时候, 才需要重新创建一个回调函数,
// 这样就保证了组件不会创建重复的回调函数.
// 而接收这个回调函数作为属性的组件, 也不会频繁地需要重新渲染
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(
() => setCount(count + 1),
[count] // 只有当 count 发生变化时才会重新创建回调函数
);
// ...
return <button onClick={handleIncrement}>+</button>;
}
useMemo 缓存计算的结果
useCallback 缓存的是一个函数, 而 useMemo 缓存的是计算的结果
useMemo 两个好处:
- 避免重复计算
- 避免子组件的重复渲染
useMemo(fn, deps);
fn: 产生所需数据的计算函数, deps 依赖项
如果某个数据是通过其它数据计算得到的, 那么只有当用到的数据, 也就是依赖的数据发生变化的时候, 才应该需要重新计算
import React, { useState, useEffect } from "react";
export default function SearchUserList() {
const [users, setUsers] = useState(null);
const [searchKey, setSearchKey] = useState("");
useEffect(() => {
const doFetch = async () => {
// 组件首次加载时发请求获取用户数据
const res = await fetch("https://reqres.in/api/users/");
setUsers(await res.json());
};
doFetch();
}, []);
let usersToShow = null;
/* 不使用 useMemo
无论组件为何刷新, 这里一定会对数组做一次过滤的操作
重新计算 usersToShow 的值
if (users) {
usersToShow = users.data.filter((user) =>
user.first_name.includes(searchKey)
);
}
*/
// 使用 userMemo 缓存计算的结果 usersToShow
// 只有在依赖项 users, searchKey 发生变化时
// 才会重新计算 usersToShow
const usersToShow = useMemo(() => {
if (!users) return null;
return users.data.filter((user) => {
return user.first_name.includes(searchKey);
});
}, [users, searchKey]);
return (
<div>
<input
type="text"
value={searchKey}
onChange={(evt) => setSearchKey(evt.target.value)}
/>
<ul>
{usersToShow &&
usersToShow.length > 0 &&
usersToShow.map((user) => {
return <li key={user.id}>{user.first_name}</li>;
})}
</ul>
</div>
);
}
useCallback 可以用 useMemo 实现, 它们都做了同一件事情: 建立了一个绑定某个结果到依赖数据的关系. 只有当依赖变了, 这个结果才需要被重新得到.
const myEventHandler = useMemo(() => {
// 返回一个函数作为缓存结果
return () => {
// 在这里进行事件处理
};
}, [dep1, dep2]);
useRef 在多次渲染之间共享数据
const myRefContainer = useRef(initialValue);
可以把 useRef 看作是在函数组件之外创建的一个容器空间, 在这个容器上可以通过唯一的 current 属设置一个值, 从而在函数组件的多次渲染之间共享这个值
// 使用了 useRef 来创建了一个保存 window.setInterval 返回句柄的空间
// 从而能够在用户点击暂停按钮时清除定时器
// 达到暂停计时的目的
import React, { useState, useCallback, useRef } from "react";
export default function Timer() {
// 定义 time state 用于保存计时的累积时间
const [time, setTime] = useState(0);
// 定义 timer 这样一个容器用于在跨组件渲染之间保存一个变量
const timer = useRef(null);
// 开始计时的事件处理函数
const handleStart = useCallback(() => {
// 使用 current 属性设置 ref 的值
timer.current = window.setInterval(() => {
setTime((time) => time + 1);
}, 100);
}, []);
// 暂停计时的事件处理函数
const handlePause = useCallback(() => {
// 使用 clearInterval 来停止计时
window.clearInterval(timer.current);
timer.current = null;
}, []);
return (
<div>
{time / 10} seconds.
<br />
<button onClick={handleStart}>Start</button>
<button onClick={handlePause}>Pause</button>
</div>
);
}
useRef 保存的数据一般是和 UI 的渲染无关的, 因此当 ref 的值发生变化时, 是不会触发组件的重新渲染的, 这也是 useRef 区别于 useState 的地方
除了存储跨渲染的数据之外, useRef 还有一个重要的功能: 保存某个 DOM 节点的引用
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// current 属性指向了真实的 input 这个 DOM 节点
// 从而可以调用 focus 方法
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
useContext 定义全局状态
React 组件之间通过 props 传值, 且只能在父子组件之间传值
React 提供了 Context 这样一个机制, 能够让所有在某个组件开始的组件树上创建一个 Context. 这样这个组件树上的所有组件就都能访问和修改这个 Context
当这个 Context 的数据发生变化时, 使用这个数据的组件就能够自动刷新, 达到数据的绑定的目的
useContext:
const value = useContext(MyContext);
创建 Context
const MyContext = React.createContext(initialValue);
MyContext 具有一个 Provider 的属性, 一般是作为组件树的根组件
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee",
},
dark: {
foreground: "#ffffff",
background: "#222222",
},
};
// 创建一个 Theme 的 Context
const ThemeContext = React.createContext(themes.light);
function App() {
// 整个应用使用 ThemeContext.Provider 作为根组件
return (
// 使用 themes.dark 作为当前 Context
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
// 在 Toolbar 组件中使用一个会使用 Theme 的 Button
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
// 在 Theme Button 中使用 useContext 来获取当前的主题
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button
style={{
background: theme.background,
color: theme.foreground,
}}
>
I am styled by theme context!
</button>
);
}
themes.dark 是作为一个属性值传给 Provider 这个组件, 如果要让它变得动态, 只要用一个 state 来保存, 通过修改 state 就能实现动态的切换 Context 的值. 而且这么做所有用到这个 Context 的地方都会自动刷新
function App() {
// 使用 state 来保存 theme 从而可以动态修改
const [theme, setTheme] = useState("light");
// 切换 theme 的回调函数
const toggleTheme = useCallback(() => {
setTheme((theme) => (theme === "light" ? "dark" : "light"));
}, []);
return (
// 使用 theme state 作为当前 Context
<ThemeContext.Provider value={themes[theme]}>
<button onClick={toggleTheme}>Toggle Theme</button>
<Toolbar />
</ThemeContext.Provider>
);
}
自定义 Hook
典型的四个使用场景
-
抽取业务逻辑
计数器的例子可以改成
import { useState, useCallback } from "react"; function useCounter() { // 定义 count 这个 state 用于保存当前数值 const [count, setCount] = useState(0); // 实现加 1 的操作 const increment = useCallback(() => setCount(count + 1), [count]); // 实现减 1 的操作 const decrement = useCallback(() => setCount(count - 1), [count]); // 重置计数器 const reset = useCallback(() => setCount(0), []); // 将业务逻辑的操作 export 出去供调用者使用 return { count, increment, decrement, reset }; } import React from "react"; function Counter() { // 调用自定义 Hook const { count, increment, decrement, reset } = useCounter(); // 渲染 UI return ( <div> <button onClick={decrement}> - </button> <p>{count}</p> <button onClick={increment}> + </button> <button onClick={reset}> reset </button> </div> ); } -
封装通用逻辑
场景: 从服务端获取用户列表, 并显示在界面上
在处理这类请求的时候, 模式都是类似的, 通常都会遵循下面步骤:
- 创建 data, loading, error 这 3 个 state
- 请求发出后, 设置 loading state 为 true
- 请求成功后, 将返回的数据放到某个 state 中, 并将 loading state 设为 false
- 请求失败后, 设置 error state 为 true, 并将 loading state 设为 false.
- 最后, 基于 data, loading, error 这 3 个 state 的数据, UI 就可以正确地显示数据, 或者 loading, error 这些反馈给用户
通过创建自定义 Hook useAsync 可以将这样的逻辑提取出来成为一个可重用的模块
import { useState } from "react"; const useAsync = (asyncFunction) => { // 设置三个异步逻辑相关的 state const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // 定义一个 callback 用于执行异步逻辑 const execute = useCallback(() => { // 请求开始时, 设置 loading 为 true, 清除已有数据和 error 状态 setLoading(true); setData(null); setError(null); return asyncFunction() .then((response) => { // 请求成功时, 将数据写进 state, 设置 loading 为 false setData(response); setLoading(false); }) .catch((error) => { // 请求失败时, 设置 loading 为 false, 并设置错误状态 setError(error); setLoading(false); }); }, [asyncFunction]); return { execute, loading, data, error }; };import React from "react"; import useAsync from './useAsync'; export default function UserList() { // 通过 useAsync 这个函数, 只需要提供异步逻辑的实现 const { execute: fetchUsers, data: users, loading, error, } = useAsync(async () => { const res = await fetch("https://reqres.in/api/users/"); const json = await res.json(); return json.data; }); return ( // 根据状态渲染 UI... ); } -
监听浏览器状态
const useWindowWidth = () => { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const onResize = () => setWidth(window.innerWidth); window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, []); return width; }; const MyComponent = () => { const width = useWindowWidth(); return <div>Window width is: {width}</div>; }; -
拆分复杂组件
参考: