为什么出现 Hooks
Hooks 就是把某个目标结果钩到某个可能会变化的数据源或事件源上,当数据源或事件源发生变化时,产生目标结果的代码会重新执行,产生更新后的结果。
- props 传递过来的值
- 从 URL 中读到的值
- 从 cookie、localStorage 中读取的值
useEffect
副作用 useEffect 在每一次快照中会将其 Array Dependency 中的 state 和 返回的 cleanup 方法存储在自己的 hooks[index] 中。在下一次更新时会先执行 cleanup 方法,然后对比依赖的state 与上一次相比是否发生变化,进而决定副作用回调方法是否执行。
useEffect(cb, depArray) {
const hasNoDeps = !depArray;
hooks[idx] = hooks[idx] || {};
const {deps, cleanup} = hooks[idx]; // undefined when first render
const hasChanged = deps
? !depArray.every((el, i) => el === deps[i])
: true;
if (hasNoDeps || hasChanged) {
cleanup && cleanup();
hooks[idx].cleanup = cb();
hooks[idx].deps = depArray;
}
idx++;
}
使用原则:
- 在复杂的副作用里,将逻辑拆分为 action 和 callback 两部分,这与 flux 思想类似:useEffect 中避免直接修改 state,只能触发 action。管理 state 的逻辑放在 callback 中,通过侦听 action 来执行具体的操作;
- 管理 state 逻辑的 callback 要么通过 setState 的参数获取所需 state,要么通过 useReducer,我们可以在 reducer 参数里获取到全部 state;
useEffect 的 Array Dependency 里只包含触发 action 的变量;
const UserContainer = () => {
const [user, setUser] = useState(null);
// 使用 useCallback 包裹
const handleUserFetch = useCalllback(async () => {
const result = await fetchUserAction();
setUser(result);
}, []);
useEffect(() => {
handleUserFetch();
}, [handleUserFetch]); /* 将 handleUserFetch 作为依赖项传入 */
if (!user) return <p>No data available.</p>
return <UserCard data={user} />
};
useRef
在函数组件之外创建的一个容器空间。在这个容器上,我们可以通过唯一的 current 属设置一个值,从而在函数组件的多次渲染之间共享这个值。
- 在多次渲染之间共享数据
- 保存某个 DOM 节点的引用 ```java 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 (
执行一次性的初始化逻辑
```java
import { useRef } from 'react';
// 创建一个自定义 Hook 用于执行一次性代码
function useSingleton(callback) {
// 用一个 called ref 标记 callback 是否执行过
const called = useRef(false);
// 如果已经执行过,则直接返回
if (called.current) return;
// 第一次调用时直接执行
callBack();
// 设置标记为已执行过
called.current = true;
}
import useSingleton from './useSingleton';
const MyComp = () => {
// 使用自定义 Hook
useSingleton(() => {
console.log('这段代码只执行一次');
});
return (
<div>My Component</div>
);
};
父组件调用子组件的方法
useImperativeHandle 可以让父组件获取并执行子组件内某些自定义函数(方法)。本质上其实是子组件将自己内部的函数(方法)通过 useImperativeHandle 添加到父组件中 useRef 定义的对象中。
1、useRef 创建引用变量
2、React.forwardRef 将引用变量传递给子组件
3、useImperativeHandle 将子组件内定义的函数作为属性,添加到父组件中的 ref 对象上。
import React,{useState,useImperativeHandle} from 'react'
function ChildComponent(props,ref) {
const [count,setCount] = useState(0); //子组件定义内部变量count
//子组件定义内部函数 addCount
const addCount = () => {
setCount(count + 1);
}
//子组件通过useImperativeHandle函数,将addCount函数添加到父组件中的ref.current中
useImperativeHandle(ref,() => ({addCount}));
return (
<div>
{count}
<button onClick={addCount}>child</button>
</div>
)
}
//子组件导出时需要被React.forwardRef包裹,否则无法接收 ref这个参数
export default React.forwardRef(ChildComponent);
import React,{useRef} from 'react'
import ChildComponent from './childComponent'
function Imperative() {
const childRef = useRef(null); //父组件定义一个对子组件的引用
const clickHandle = () => {
childRef.current.addCount(); //父组件调用子组件内部 addCount函数
}
return (
<div>
{/* 父组件通过给子组件添加 ref 属性,将childRef传递给子组件,
子组件获得该引用即可将内部函数添加到childRef中 */}
<ChildComponent ref={childRef} />
<button onClick={clickHandle}>child component do somting</button>
</div>
)
}
export default Imperative;
自定义 Hooks
封装通用逻辑
在 Hooks 中,你可以管理当前组件的 state,从而将更多的逻辑写在可重用的 Hooks 中。 普通的工具类中无法直接修改组件 state ,那么也就无法在数据改变的时候触发组件的重新渲染。
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...
);
}
监听数据源状态
Hooks 可以让 React 的组件绑定在任何可能的数据源上。这样当数据源发生变化时,组件能够自动刷新。
import { useState, useEffect } from 'react';
// 获取横向,纵向滚动条位置
const getPosition = () => {
return {
x: document.body.scrollLeft,
y: document.body.scrollTop,
};
};
const useScroll = () => {
// 定一个 position 这个 state 保存滚动条位置
const [position, setPosition] = useState(getPosition());
useEffect(() => {
const handler = () => {
setPosition(getPosition(document));
};
// 监听 scroll 事件,更新滚动条位置
document.addEventListener("scroll", handler);
return () => {
// 组件销毁时,取消事件监听
document.removeEventListener("scroll", handler);
};
}, []);
return position;
};
import React, { useCallback } from 'react';
import useScroll from './useScroll';
function ScrollTop() {
const { y } = useScroll();
const goTop = useCallback(() => {
document.body.scrollTop = 0;
}, []);
const style = {
position: "fixed",
right: "10px",
bottom: "10px",
};
// 当滚动条位置纵向超过 300 时,显示返回顶部按钮
if (y > 300) {
return (
<button onClick={goTop} style={style}>
Back to Top
</button>
);
}
// 否则不 render 任何 UI
return null;
}
拆分复杂组件
把 Hooks 就看成普通的函数,能隔离的尽量去做隔离,从而让代码更加模块化 尽量将相关的逻辑做成独立的 Hooks,然后在函数组中使用这些 Hooks,通过参数传递和返回值让 Hooks 之间完成交互。
import React, { useEffect, useCallback, useMemo, useState } from "react";
import { Select, Table } from "antd";
import _ from "lodash";
import useAsync from "./useAsync";
const endpoint = "https://myserver.com/api/";
const useArticles = () => {
// 使用上面创建的 useAsync 获取文章列表
const { execute, data, loading, error } = useAsync(
useCallback(async () => {
const res = await fetch(`${endpoint}/posts`);
return await res.json();
}, []),
);
// 执行异步调用
useEffect(() => execute(), [execute]);
// 返回语义化的数据结构
return {
articles: data,
articlesLoading: loading,
articlesError: error,
};
};
const useCategories = () => {
// 使用上面创建的 useAsync 获取分类列表
const { execute, data, loading, error } = useAsync(
useCallback(async () => {
const res = await fetch(`${endpoint}/categories`);
return await res.json();
}, []),
);
// 执行异步调用
useEffect(() => execute(), [execute]);
// 返回语义化的数据结构
return {
categories: data,
categoriesLoading: loading,
categoriesError: error,
};
};
const useCombinedArticles = (articles, categories) => {
// 将文章数据和分类数据组合到一起
return useMemo(() => {
// 如果没有文章或者分类数据则返回 null
if (!articles || !categories) return null;
return articles.map((article) => {
return {
...article,
category: categories.find(
(c) => String(c.id) === String(article.categoryId),
),
};
});
}, [articles, categories]);
};
const useFilteredArticles = (articles, selectedCategory) => {
// 实现按照分类过滤
return useMemo(() => {
if (!articles) return null;
if (!selectedCategory) return articles;
return articles.filter((article) => {
console.log("filter: ", article.categoryId, selectedCategory);
return String(article?.category?.name) === String(selectedCategory);
});
}, [articles, selectedCategory]);
};
const columns = [
{ dataIndex: "title", title: "Title" },
{ dataIndex: ["category", "name"], title: "Category" },
];
export default function BlogList() {
const [selectedCategory, setSelectedCategory] = useState(null);
// 获取文章列表
const { articles, articlesError } = useArticles();
// 获取分类列表
const { categories, categoriesError } = useCategories();
// 组合数据
const combined = useCombinedArticles(articles, categories);
// 实现过滤
const result = useFilteredArticles(combined, selectedCategory);
// 分类下拉框选项用于过滤
const options = useMemo(() => {
const arr = _.uniqBy(categories, (c) => c.name).map((c) => ({
value: c.name,
label: c.name,
}));
arr.unshift({ value: null, label: "All" });
return arr;
}, [categories]);
// 如果出错,简单返回 Failed
if (articlesError || categoriesError) return "Failed";
// 如果没有结果,说明正在加载
if (!result) return "Loading...";
return (
<div>
<Select
value={selectedCategory}
onChange={(value) => setSelectedCategory(value)}
options={options}
style={{ width: "200px" }}
placeholder="Select a category"
/>
<Table dataSource={result} columns={columns} />
</div>
);
}
复杂状态管理:如何保证状态一致性
- 保证状态最小化
- 避免中间状态,确保唯一数据源
保证状态最小化
某些数据如果能从已有的 State 中计算得到,那么我们就应该始终在用的时候去计算,而不要把计算的结果存到某个 State 中。 实用 useMemo 缓存计算的结果
function FilterList({ data }) {
// 设置关键字的 State
const [searchKey, setSearchKey] = useState('');
// 设置最终要展示的数据状态,并用原始数据作为初始值
const [filtered, setFiltered] = useState(data);
// 在 data 变化的时候,也重新生成最终数据
useEffect(() => { setFiltered(data => {...}) }, [data, searchKey])
// 处理用户的搜索关键字
const handleSearch = useCallback(evt => {
setSearchKey(evt.target.value);
setFiltered(data.filter(item => {
return item.title.includes(evt.target.value)));
}));
}, [filtered])
return (
<div>
<input value={searchKey} onChange={handleSearch} />
{/* 根据 filtered 数据渲染 UI */}
</div>
);
}
import React, { useState, useMemo } from "react";
function FilterList({ data }) {
const [searchKey, setSearchKey] = useState("");
// 每当 searchKey 或者 data 变化的时候,重新计算最终结果
const filtered = useMemo(() => {
return data.filter((item) =>
item.title.toLowerCase().includes(searchKey.toLowerCase())
);
}, [searchKey, data]);
return (
<div className="08-filter-list">
<h2>Movies</h2>
<input
value={searchKey}
placeholder="Search..."
onChange={(evt) => setSearchKey(evt.target.value)}
/>
<ul style={{ marginTop: 20 }}>
{filtered.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
避免中间状态,确保唯一数据源
异步处理:如何向服务器发送请求
无论大小项目,在开始实现第一个请求的时候,通常我们要做的第一件事应该都是创建一个自己的 API Client,之后所有的请求都会通过这个 Client 发出去。
封装远程资源
Hooks 的一个特性是可以绑定任何数据源,可以把 Get 请求当作一个远程数据源
import { useState, useEffect } from "react";
import apiClient from "./apiClient";
// 将获取文章的 API 封装成一个远程资源 Hook
const useArticle = (id) => {
// 设置三个状态分别存储 data, error, loading
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// 重新获取数据时重置三个状态
setLoading(true);
setData(null);
setError(null);
apiClient
.get(`/posts/${id}`)
.then((res) => {
// 请求成功时设置返回数据到状态
setLoading(false);
setData(res.data);
})
.catch((err) => {
// 请求失败时设置错误状态
setLoading(false);
setError(err);
});
}, [id]); // 当 id 变化时重新获取数据
// 将三个状态作为 Hook 的返回值
return {
loading,
error,
data
};
};
函数组件设计模式:如何应对复杂条件渲染场景
容器模式:实现按条件执行 Hooks
把原来需要条件运行的 Hooks 拆分成子组件,然后通过一个容器组件来进行实际的条件判断,从而渲染不同的组件,实现按条件渲染的目的。
- Hooks 必须按顺序被执行到,不能放到条件判断、循环等语句中。
```javascript
// 定义一个容器组件用于封装真正的 UserInfoModal
export default function UserInfoModalWrapper({
visible,
…rest, // 使用 rest 获取除了 visible 之外的属性
}) {
// 如果对话框不显示,则不 render 任何内容
if (!visible) return null;
// 否则真正执行对话框的组件逻辑
return
; }
<a name="w6xdr"></a>
## 使用 render props 模式重用 UI 逻辑
> render props 就是把一个 render 函数作为属性传递给某个组件,由这个组件去执行这个函数从而 render 实际的内容
> Hooks 有一个局限,那就是只能用作数据逻辑的重用,而一旦涉及 UI 表现逻辑的重用,而这可以使用 render props 来解决
```javascript
import { useState, useCallback } from "react";
function CounterRenderProps({ children }) {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
const decrement = useCallback(() => {
setCount(count - 1);
}, [count]);
return children({ count, increment, decrement });
}
function CounterRenderPropsExample() {
return (
<CounterRenderProps>
{({ count, increment, decrement }) => {
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}}
</CounterRenderProps>
);
}
事件处理:如何创建自定义事件
是否需要 useCallback ,和函数的复杂度没有必然关系,而是和回调函数绑定到哪个组件有关。这是为了避免因组件属性变化而导致不必要的重新渲染。
- 原生的 DOM 节点,比如 button、input 等,不用担心重新渲染的
- 自定义组件,或者一些 UI 框架的组件需要考虑性能问题
Hooks 具备绑定任何数据源的能力 使用 Hooks 封装事件,相当于将这些动作事件看作不断变化的数据源
import { useEffect, useState } from "react";
// 使用 document.body 作为默认的监听节点
const useKeyPress = (domNode = document.body) => {
const [key, setKey] = useState(null);
useEffect(() => {
const handleKeyPress = (evt) => {
setKey(evt.keyCode);
};
// 监听按键事件
domNode.addEventListener("keypress", handleKeyPress);
return () => {
// 接触监听按键事件
domNode.removeEventListener("keypress", handleKeyPress);
};
}, [domNode]);
return key;
};
import useKeyPress from './useKeyPress';
function UseKeyPressExample() => {
const key = useKeyPress();
return (
<div>
<h1>UseKeyPress</h1>
<label>Key pressed: {key || "N/A"}</label>
</div>
);
};
使用浮动层:如何展示对话框,并给对话框传递参数
一个实现业务逻辑的 Modal 究竟应该在哪个组件中去声明?又该怎么和它进行交互呢?
如何用一个统一的方式去管理对话框,从而让对话框相关的业务逻辑能够更加模块化,以及和其他业务逻辑进行解耦。
使用全局状态管理所有对话框
对话框这种模式的本质就是一个独立的窗口,它和一个拥有独立 URL 的页面在功能上和形式上都是极为类似的
处理对话框返回值
把用户在对话框中的操作看成一个异步操作逻辑,那么用户在完成了对话框中内容的操作之后,就认为异步逻辑完成了
const modal = useNiceModal('my-modal');
// 实现一个 promise API 来处理返回值
modal.show(args).then(result => {});
// 使用一个 object 缓存 promise 的 resolve 回调函数
const modalCallbacks = {};
export const useNiceModal = (modalId) => {
const dispatch = useDispatch();
const show = useCallback(
(args) => {
return new Promise((resolve) => {
// 显示对话框时,返回 promise 并且将 resolve 方法临时存起来
modalCallbacks[modalId] = resolve;
dispatch(showModal(modalId, args));
});
},
[dispatch, modalId],
);
const resolve = useCallback(
(args) => {
if (modalCallbacks[modalId]) {
// 如果存在 resolve 回调函数,那么就调用
modalCallbacks[modalId](args);
// 确保只能 resolve 一次
delete modalCallbacks[modalId];
}
},
[modalId],
);
// 其它逻辑...
// 将 resolve 也作为返回值的一部分
return { show, hide, resolve, visible, hiding };
};
路由管理
- 提供了按页面去组织整个应用程序的能力
统一资源定位符:对于能够通过前端应用展现的每一个资源,你都要考虑 URL 是否能唯一地定位到这个资源
按需加载
使用 import 语句,定义按需加载的起始模块
function ProfilePage() {
// 定义一个 state 用于存放需要加载的组件
const [RealPage, setRealPage] = useState(null);
// 根据路径动态加载真正的组件实现
import('./RealProfilePage').then((comp) => {
setRealPage(Comp);
});
// 如果组件未加载则显示 Loading 状态
if (!RealPage) return 'Loading....';
// 组件加载成功后则将其渲染到界面
return <RealPage />
}
import() 这个语句完全是由 Webpack 进行处理的。Webpack 会将以“./RealProfilePage”模块为起点的所有依赖模块,单独打成一个包。并且,Webpack 还会生成代码,用于按需加载这个模块。
按业务模块为目标去做隔离,尽量在每个模块的起始页面去定义这个拆分点
使用 react-lodable,实现组件的异步加载
```javascript import Loadable from “react-loadable”;
// 创建一个显示加载状态的组件 function Loading({ error }) { return error ? ‘Failed’ : ‘Loading’; } // 创建加载器组件 const HelloLazyLoad = Loadable({ loader: () => import(“./RealHelloLazyLoad”), loading: Loading, }); ```