image.png

这里推荐两个强大的 React Hooks 库:React UseUmi Hooks。它们都实现了很多生产级别的自定义 Hook,非常值得学习。

基本定义

一个简单的自定义Hook

  1. function useBodyScrollPosition() {
  2. const [scrollPosition, setScrollPosition] = useState(null);
  3. useEffect(() => {
  4. const handleScroll = () => setScrollPosition(window.scrollY);
  5. document.addEventListener('scroll', handleScroll);
  6. return () =>
  7. document.removeEventListener('scroll', handleScroll);
  8. }, []);
  9. return scrollPosition;
  10. }

通过观察,我们可以发现自定义 Hook 具有以下特点:

  • 表面上:一个命名格式为 useXXX 的函数,但不是 React 函数式组件
  • 本质上:内部通过使用 React 自带的一些 Hook (例如 useState 和 useEffect )来实现某些通用的逻辑

如果你发散一下思维,可以想到有很多地方可以去做自定义 Hook:DOM 副作用修改/监听、动画、请求、表单操作、数据存储等等。

内部原理

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

v2-04a5b174d7250948c7918c82ab3348d7_b.gif

同样地,即便代码的执行进入到自定义 Hook 中,我们依然可以从 Hook 链表中读取到相应的数据,这个”配对“的过程总能成功。

自定义 Hook 本质上只是把调用内置 Hook 的过程封装成一个个可以复用的函数,并不影响 Hook 链表的生成和读取

自定义封装

  1. import { useState, useCallback } from 'react';
  2. // 使用示例:
  3. // export default function UserList() {
  4. // const {
  5. // execute: fetchUsers,
  6. // data: users,
  7. // loading,
  8. // error,
  9. // } = useAsync(async () => {
  10. // const res = await fetch("https://reqres.in/api/users/");
  11. // const json = await res.json();
  12. // return json.data;
  13. // });
  14. // return (
  15. // // 根据状态渲染 UI...
  16. // );
  17. // }
  18. function useFetch(asyncFunction) {
  19. // 设置三个异步逻辑相关的 state
  20. const [data, setData] = useState(null);
  21. const [loading, setLoading] = useState(false);
  22. const [error, setError] = useState(null);
  23. // 定义一个 callback 用于执行异步逻辑
  24. const execute = useCallback(() => {
  25. // 请求开始时,设置 loading 为 true,清除已有数据和 error 状态
  26. setLoading(true);
  27. setData(null);
  28. setError(null);
  29. return asyncFunction()
  30. .then(response => {
  31. // 请求成功时,将数据写进 state,设置 loading 为 false
  32. setData(response);
  33. setLoading(false);
  34. })
  35. .catch(error => {
  36. // 请求失败时,设置 loading 为 false,并设置错误状态
  37. setError(error);
  38. setLoading(false);
  39. });
  40. }, [asyncFunction]);
  41. return { execute, loading, data, error };
  42. }
  43. export default useFetch;
  1. import { useState, useEffect } from 'react';
  2. // 获取横向,纵向滚动条位置
  3. const getPosition = () => {
  4. return {
  5. x: document.body.scrollLeft,
  6. y: document.body.scrollTop,
  7. };
  8. };
  9. const useScroll = () => {
  10. // 定一个 position 这个 state 保存滚动条位置
  11. const [position, setPosition] = useState(getPosition());
  12. useEffect(() => {
  13. const handler = () => {
  14. setPosition(getPosition(document));
  15. };
  16. // 监听 scroll 事件,更新滚动条位置
  17. document.addEventListener("scroll", handler);
  18. return () => {
  19. // 组件销毁时,取消事件监听
  20. document.removeEventListener("scroll", handler);
  21. };
  22. }, []);
  23. return position;
  24. };
  25. import React, { useCallback } from 'react';
  26. import useScroll from './useScroll';
  27. function ScrollTop() {
  28. const { y } = useScroll();
  29. const goTop = useCallback(() => {
  30. document.body.scrollTop = 0;
  31. }, []);
  32. const style = {
  33. position: "fixed",
  34. right: "10px",
  35. bottom: "10px",
  36. };
  37. // 当滚动条位置纵向超过 300 时,显示返回顶部按钮
  38. if (y > 300) {
  39. return (
  40. Back to Top
  41. );
  42. }
  43. // 否则不 render 任何 UI
  44. return null;
  45. }
  1. import React, { useEffect, useCallback, useMemo, useState } from "react";
  2. import { Select, Table } from "antd";
  3. import _ from "lodash";
  4. import useAsync from "./useAsync";
  5. const endpoint = "https://myserver.com/api/";
  6. const useArticles = () => {
  7. // 使用上面创建的 useAsync 获取文章列表
  8. const { execute, data, loading, error } = useAsync(
  9. useCallback(async () => {
  10. const res = await fetch(`${endpoint}/posts`);
  11. return await res.json();
  12. }, []),
  13. );
  14. // 执行异步调用
  15. useEffect(() => execute(), [execute]);
  16. // 返回语义化的数据结构
  17. return {
  18. articles: data,
  19. articlesLoading: loading,
  20. articlesError: error,
  21. };
  22. };
  23. const useCategories = () => {
  24. // 使用上面创建的 useAsync 获取分类列表
  25. const { execute, data, loading, error } = useAsync(
  26. useCallback(async () => {
  27. const res = await fetch(`${endpoint}/categories`);
  28. return await res.json();
  29. }, []),
  30. );
  31. // 执行异步调用
  32. useEffect(() => execute(), [execute]);
  33. // 返回语义化的数据结构
  34. return {
  35. categories: data,
  36. categoriesLoading: loading,
  37. categoriesError: error,
  38. };
  39. };
  40. const useCombinedArticles = (articles, categories) => {
  41. // 将文章数据和分类数据组合到一起
  42. return useMemo(() => {
  43. // 如果没有文章或者分类数据则返回 null
  44. if (!articles || !categories) return null;
  45. return articles.map((article) => {
  46. return {
  47. ...article,
  48. category: categories.find(
  49. (c) => String(c.id) === String(article.categoryId),
  50. ),
  51. };
  52. });
  53. }, [articles, categories]);
  54. };
  55. const useFilteredArticles = (articles, selectedCategory) => {
  56. // 实现按照分类过滤
  57. return useMemo(() => {
  58. if (!articles) return null;
  59. if (!selectedCategory) return articles;
  60. return articles.filter((article) => {
  61. console.log("filter: ", article.categoryId, selectedCategory);
  62. return String(article?.category?.name) === String(selectedCategory);
  63. });
  64. }, [articles, selectedCategory]);
  65. };
  66. const columns = [
  67. { dataIndex: "title", title: "Title" },
  68. { dataIndex: ["category", "name"], title: "Category" },
  69. ];
  70. export default function BlogList() {
  71. const [selectedCategory, setSelectedCategory] = useState(null);
  72. // 获取文章列表
  73. const { articles, articlesError } = useArticles();
  74. // 获取分类列表
  75. const { categories, categoriesError } = useCategories();
  76. // 组合数据
  77. const combined = useCombinedArticles(articles, categories);
  78. // 实现过滤
  79. const result = useFilteredArticles(combined, selectedCategory);
  80. // 分类下拉框选项用于过滤
  81. const options = useMemo(() => {
  82. const arr = _.uniqBy(categories, (c) => c.name).map((c) => ({
  83. value: c.name,
  84. label: c.name,
  85. }));
  86. arr.unshift({ value: null, label: "All" });
  87. return arr;
  88. }, [categories]);
  89. // 如果出错,简单返回 Failed
  90. if (articlesError || categoriesError) return "Failed";
  91. // 如果没有结果,说明正在加载
  92. if (!result) return "Loading...";
  1. import { useEffect, useState, useReducer, useRef, useContext, useCallback, useMemo } from 'react';
  2. import M from '@block/utils';
  3. import Big from 'big.js';
  4. import { useDebounce } from 'react-use';
  5. import { JD_SUPPLIER_CODE, MATERIEL_TEMPLATE_CODE } from '@/src/constant';
  6. import fetchWithCache from './fetchWithCache';
  7. import URL from '../../../url';
  8. const ERROR_MESSAGE_MAP = {
  9. cityAccountFlag: {
  10. message: '此行没有进行核算城市配置,如果需要的话请联系对应财务FP在海鸥的“核算城市配置”界面进行配置。',
  11. type: 'info',
  12. },
  13. coa: {
  14. message: '没有获取到 COA 科目,请联系FP维护!',
  15. type: 'error',
  16. },
  17. budget: {
  18. message: '没有获取到预算科目,请联系FP维护!',
  19. type: 'error',
  20. },
  21. budgetType: {
  22. message: '此行品类为资产黑名单,不允许选择当前预算项目&扩展项目组合,请重新选择,如有疑问请联系您的财务FP!',
  23. type: 'error',
  24. },
  25. delivery: {
  26. message: '该商品暂时无法配送至此地址!',
  27. type: 'error',
  28. },
  29. stock: {
  30. message: '商品库存不足,请重新调整数量!',
  31. type: 'error',
  32. },
  33. offsale: {
  34. message: '商品已下架,无法购买!',
  35. type: 'error',
  36. },
  37. sale: {
  38. message: '该商品暂不支持售卖!',
  39. type: 'error',
  40. },
  41. material: {
  42. message: '该物料已失效,请删除或重新选择物料后提交申请',
  43. type: 'error',
  44. },
  45. };
  46. function errorReducer(state, action) {
  47. const tempState = state;
  48. const { type, show } = action;
  49. if (ERROR_MESSAGE_MAP[type]) {
  50. tempState[type] = show ? ERROR_MESSAGE_MAP[type] : undefined;
  51. }
  52. return {
  53. ...tempState,
  54. };
  55. }
  56. export default function useValidateProduct(
  57. data,
  58. onChange,
  59. isKuaiLv,
  60. isXiaoXiang,
  61. commonInfo,
  62. isNon,
  63. onFetchJDGoodsStockStatus,
  64. ) {
  65. const [errors, dispatchError] = useReducer(errorReducer, {});
  66. const [loading, setLoading] = useState(false);
  67. // 采购数量更改 如果是京东商品 校验京东库存
  68. useEffect(() => {
  69. const fetchJDStock = async () => {
  70. const res = await M.$fetch(URL.JD.STOCK, { goodsCode, goodsQuantity: quantity, reqAddressResp });
  71. onChange({ goodStockStatus: res.goodStockStatus, prLineNo });
  72. };
  73. if (isJD) {
  74. fetchJDStock();
  75. }
  76. }, [quantity]);
  77. // *******************************✂️ 华丽分割线 ✂️*************************
  78. const errorArray = Object.keys(errors)
  79. .map(key => errors[key])
  80. .filter(e => e);
  81. return [loading, errorArray];
  82. }
  1. const [computing, errors] = useValidateProduct(
  2. data,
  3. onChange,
  4. );

References

https://zhuanlan.zhihu.com/p/134030294