这里推荐两个强大的 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) {
// 设置三个异步逻辑相关的 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 };
}
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 任何 UI
return 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(() => {
// 如果没有文章或者分类数据则返回 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...";
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,
);