
这里推荐两个强大的 React Hooks 库:React Use 和 Umi Hooks。它们都实现了很多生产级别的自定义 Hook,非常值得学习。
基本定义
一个简单的自定义Hook
function useBodyScrollPosition() {const [scrollPosition, setScrollPosition] = useState(null);useEffect(() => {const handleScroll = () => setScrollPosition(window.scrollY);document.addEventListener('scroll', handleScroll);return () =>document.removeEventListener('scroll', handleScroll);}, []);return scrollPosition;}
通过观察,我们可以发现自定义 Hook 具有以下特点:
- 表面上:一个命名格式为 useXXX 的函数,但不是 React 函数式组件
 - 本质上:内部通过使用 React 自带的一些 Hook (例如 useState 和 useEffect )来实现某些通用的逻辑
 
如果你发散一下思维,可以想到有很多地方可以去做自定义 Hook:DOM 副作用修改/监听、动画、请求、表单操作、数据存储等等。
内部原理

可以看到,即便我们切换到了自定义 Hook 中,Hook 链表的生成依旧没有改变。再来看看重渲染的情况:

同样地,即便代码的执行进入到自定义 Hook 中,我们依然可以从 Hook 链表中读取到相应的数据,这个”配对“的过程总能成功。
自定义 Hook 本质上只是把调用内置 Hook 的过程封装成一个个可以复用的函数,并不影响 Hook 链表的生成和读取。
自定义封装
import { useState, useCallback } from 'react';// 使用示例:// export default function UserList() {// 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...// );// }function useFetch(asyncFunction) {// 设置三个异步逻辑相关的 stateconst [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 为 falsesetData(response);setLoading(false);}).catch(error => {// 请求失败时,设置 loading 为 false,并设置错误状态setError(error);setLoading(false);});}, [asyncFunction]);return { execute, loading, data, error };}export default useFetch;
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 (Back to Top);}// 否则不 render 任何 UIreturn null;}
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(() => {// 如果没有文章或者分类数据则返回 nullif (!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]);// 如果出错,简单返回 Failedif (articlesError || categoriesError) return "Failed";// 如果没有结果,说明正在加载if (!result) return "Loading...";
import { useEffect, useState, useReducer, useRef, useContext, useCallback, useMemo } from 'react';import M from '@block/utils';import Big from 'big.js';import { useDebounce } from 'react-use';import { JD_SUPPLIER_CODE, MATERIEL_TEMPLATE_CODE } from '@/src/constant';import fetchWithCache from './fetchWithCache';import URL from '../../../url';const ERROR_MESSAGE_MAP = {cityAccountFlag: {message: '此行没有进行核算城市配置,如果需要的话请联系对应财务FP在海鸥的“核算城市配置”界面进行配置。',type: 'info',},coa: {message: '没有获取到 COA 科目,请联系FP维护!',type: 'error',},budget: {message: '没有获取到预算科目,请联系FP维护!',type: 'error',},budgetType: {message: '此行品类为资产黑名单,不允许选择当前预算项目&扩展项目组合,请重新选择,如有疑问请联系您的财务FP!',type: 'error',},delivery: {message: '该商品暂时无法配送至此地址!',type: 'error',},stock: {message: '商品库存不足,请重新调整数量!',type: 'error',},offsale: {message: '商品已下架,无法购买!',type: 'error',},sale: {message: '该商品暂不支持售卖!',type: 'error',},material: {message: '该物料已失效,请删除或重新选择物料后提交申请',type: 'error',},};function errorReducer(state, action) {const tempState = state;const { type, show } = action;if (ERROR_MESSAGE_MAP[type]) {tempState[type] = show ? ERROR_MESSAGE_MAP[type] : undefined;}return {...tempState,};}export default function useValidateProduct(data,onChange,isKuaiLv,isXiaoXiang,commonInfo,isNon,onFetchJDGoodsStockStatus,) {const [errors, dispatchError] = useReducer(errorReducer, {});const [loading, setLoading] = useState(false);// 采购数量更改 如果是京东商品 校验京东库存useEffect(() => {const fetchJDStock = async () => {const res = await M.$fetch(URL.JD.STOCK, { goodsCode, goodsQuantity: quantity, reqAddressResp });onChange({ goodStockStatus: res.goodStockStatus, prLineNo });};if (isJD) {fetchJDStock();}}, [quantity]);// *******************************✂️ 华丽分割线 ✂️*************************const errorArray = Object.keys(errors).map(key => errors[key]).filter(e => e);return [loading, errorArray];}
const [computing, errors] = useValidateProduct(data,onChange,);
