如何优雅地组织业务逻辑?自定义 Hook 全解析

如何优雅地组织业务逻辑?自定义 Hook 全解析

在实际开发中我们经常会遇到一些重复性的逻辑 ,比如:数据获取、表单验证、订阅事件或 WebSocket、浏览器本地存储交互。这些逻辑如果在多个组件中重复编写,不仅会增加代码冗余,还会降低可维护性 。这时,自定义 Hook 就应运而生了------它正是解决这类问题的方法。

一、什么是自定义 Hook?

自定义 Hook 是 React 中一种将组件中可复用的逻辑抽离出来 的方式。它本质上是一个以 use 开头的 JavaScript 函数,通过引用这个函数可以实现在多个组件之间共享状态逻辑或副作用处理逻辑。

换句话说,自定义 Hook 就是封装一组相关 Hook 的组合逻辑,用于复用组件间的状态逻辑。

举个例子:假设你在开发一个网页应用,多个组件都需要知道当前鼠标的坐标(比如做一个"跟随鼠标动画"或"高亮提示"功能)。这时候就可以把这个"获取鼠标位置"的逻辑封装成一个自定义 Hook。

  • 注意要点自定义 Hook 并不共享状态本身,而是共享状态逻辑。每个使用该 Hook 的组件仍然拥有独立的状态。

    或许你会有这样的疑问:自定义 Hook 中确实声明了状态(比如用 useState),那为什么说它不共享状态本身呢?

    useCounter 自定义 Hook 为例,这个Hook用于统计数量,hook内有一个数据状态count

    js 复制代码
    function useCounter() {
      const [count, setCount] = useState(0);
      
      const increment = () => setCount(c => c + 1);
      const decrement = () => setCount(c => c - 1);
    
      return { count, increment, decrement };
    }

    在两个组件中分别使用这个 Hook:

    jsx 复制代码
    function ComponentA() {
      const { count, increment } = useCounter();
      return (
        <div>
          <h2>Component A: {count}</h2>
          <button onClick={increment}>Increment</button>
        </div>
      );
    }
    
    function ComponentB() {
      const { count, increment } = useCounter();
      return (
        <div>
          <h2>Component B: {count}</h2>
          <button onClick={increment}>Increment</button>
        </div>
      );
    }

    当我们点击 ComponentA 的按钮,只会改变 ComponentA 中的 count,点击 ComponentB 的按钮,只会改变 ComponentB 中的 count

    为什么会这样

    这是因为每次调用 useCounter() 时,React 都会在当前组件的上下文中重新执行这个函数,并为该组件创建一份新的状态(可以理解为闭包)。

    • 每个组件在调用 useState() 时,React 内部都会为它分配一块"私有内存区域"来保存状态。

    • 即使多个组件调用了同一个 Hook,它们各自的状态仍然是隔离的。

  • 自定义 Hook 的优势

    提高代码复用率:将通用逻辑抽取到自定义 Hook 中,避免在多个组件中重复实现相同的副作用或状态管理逻辑。

    增强组件职责单一性:组件只负责 UI 层逻辑,数据处理、副作用等都通过 Hook 抽离出去,使得组件更加清晰易读。

    更好的测试性与可维护性:Hook 可以单独测试,也可以按需修改而不影响组件结构。

    支持跨项目复用:一旦封装成通用 Hook,就可以打包发布到 npm 或内部库中,供多个项目复用。

二、自定义 Hook 的基本结构

创建自定义 Hook 的过程其实很简单,就是定义一个以 use 开头的函数,并在其中使用 React 的内置 Hooks 来实现特定的功能逻辑。

  • 设计原则 :每个 Hook 应该只做一件事情。尽量避免在 Hook 内部产生副作用,比如直接操作 DOM 或者执行异步请求等。如果不可避免,请使用 useEffect。自定义 Hook 应该是纯粹的函数,即给定相同的输入总是返回相同的结果。

  • 注意事项

    1. 命名规范
    • 所有自定义 Hook 必须以 use 开头,如 useWindowSize, useInput, useLocalStorage

    • 避免与内置 Hook 冲突,如不要命名为 useState, useEffect

    1. 只在顶层调用 Hook,不能在循环、条件语句或嵌套函数中调用 Hook,否则会导致 React 的 Hook 调用顺序混乱,出现不可预知的 bug。

    2. 明确返回值结构,尽量返回一个对象或数组,方便使用者解构使用。例如:

    js 复制代码
    const { data, loading, error } = useFetch(url);
  • 示例

    下面是一个简单的例子,演示如何创建一个自定义 Hook 来追踪窗口大小:

    js 复制代码
    import { useState, useEffect } from 'react';
    
    function useWindowSize() {
      const [windowSize, setWindowSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    
      useEffect(() => {
        function handleResize() {
          setWindowSize({
            width: window.innerWidth,
            height: window.innerHeight,
          });
        }
    
        window.addEventListener('resize', handleResize);
        return () => window.removeEventListener('resize', handleResize);
      }, []);
    
      return windowSize;
    }

    然后在任意组件中都可以这样使用:

    jsx 复制代码
    function WindowDisplay() {
      const size = useWindowSize();
    
      return (
        <div>
          窗口尺寸:{size.width} x {size.height}
        </div>
      );
    }

三、自定义 Hook 的应用场景

  1. 数据请求与缓存

    我们可以封装一个统一的数据请求 Hook,支持 loading、error、缓存等功能:

    js 复制代码
    function useFetch(url) {
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState(null);
    
      useEffect(() => {
        fetch(url)
          .then(res => res.json())
          .then(data => {
            setData(data);
            setLoading(false);
          })
          .catch(err => {
            setError(err);
            setLoading(false);
          });
      }, [url]);
    
      return { data, loading, error };
    }
  2. 用户认证状态管理

    例如封装一个 useAuth() Hook,用于检查当前用户是否登录、是否有权限访问某些资源。

    js 复制代码
    function useAuth() {
      const [isAuthenticated, setIsAuthenticated] = useState(!!localStorage.getItem('token'));
    
      function login(token) {
        localStorage.setItem('token', token);
        setIsAuthenticated(true);
      }
    
      function logout() {
        localStorage.removeItem('token');
        setIsAuthenticated(false);
      }
    
      return { isAuthenticated, login, logout };
    }
  3. 表单验证逻辑

    我们可以封装一个通用的表单 Hook,支持输入绑定、验证规则、错误提示等。

    js 复制代码
    function useForm(initialState, validate) {
      const [values, setValues] = useState(initialState);
      const [errors, setErrors] = useState({});
      const [touched, setTouched] = useState({});
    
      const handleChange = e => {
        const { name, value } = e.target;
        setValues(prev => ({ ...prev, [name]: value }));
      };
    
      const handleBlur = e => {
        const { name } = e.target;
        setTouched(prev => ({ ...prev, [name]: true }));
        const validationErrors = validate(values);
        setErrors(validationErrors);
      };
    
      const handleSubmit = callback => e => {
        e.preventDefault();
        const validationErrors = validate(values);
        setErrors(validationErrors);
        if (Object.keys(validationErrors).length === 0) {
          callback();
        }
      };
    
      return { values, errors, touched, handleChange, handleBlur, handleSubmit };
    }
相关推荐
一斤代码2 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子2 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年2 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子3 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina3 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路3 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_4 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
甜瓜看代码4 小时前
1.
react.js·node.js·angular.js
伍哥的传说4 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409194 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app