掘金文章归档 · 第 16 篇 React 系列 3/4 状态管理

React 自定义 Hook:能否作为模块级全局状态管理?

模块级作用域闭包缓存原理拆解,3 个递进示例 + state 是否重复初始化验证

原文作者:随意_(掘金) | juejin.cn/post/7575887863796924426 | 发布于 2025-11-24 | 阅读 227 · 约 11 分钟

0先明确结论

在 React 开发中,状态管理是核心需求之一。从简单的组件内 state,到 Context API、Redux 等全局状态方案,开发者一直在根据场景选择合适的方案。而自定义 Hook 作为 React 的核心特性,因其复用逻辑的能力,常常被开发者尝试用于全局状态管理。

📌 三个结论:
  1. 自定义 Hook 可以实现模块级全局状态管理,但有适用场景(小型应用、非复杂状态);
  2. 核心原理是模块级作用域的闭包缓存——状态存储在模块的闭包中,而非组件实例,跨组件复用 Hook 时共享同一状态;
  3. 每次组件引入并调用 Hook 时,内部的 state 不会重新执行初始化,而是复用模块闭包中缓存的状态(仅组件渲染时重新执行 Hook 逻辑,但 state 引用不变)。

1核心原理:模块作用域闭包缓存

1.1 模块作用域:状态的"全局容器"

在 ES6 模块系统中,每个 .js 文件都是一个独立的模块,模块内的变量、函数会形成模块级作用域——即模块被导入时,会先执行一次初始化,之后所有导入该模块的地方,共享的是同一个模块实例,模块内的变量不会被重复创建。

1.2 React Hook 的"状态绑定":依赖闭包而非组件实例

当我们在模块中定义一个自定义 Hook,并在 Hook 内部调用 useState 时,这个 useState 的状态并不会绑定到某个具体组件,而是绑定到"模块级闭包"中。因为:

1.3 关键区别:普通自定义 Hook vs 全局状态自定义 Hook

特性普通自定义 Hook(如 useCounter)全局状态自定义 Hook(如 useGlobalUser)
状态存储位置组件实例的 Hook 链表中模块作用域的闭包中
状态共享性组件私有(每个组件调用创建独立状态)跨组件共享(所有组件调用共享同一状态)
state 初始化次数每个组件调用一次模块初始化时仅一次
Hook 逻辑执行次数组件每次渲染执行组件每次渲染执行(但仅读缓存)
📌 核心差异:将 useState 的初始化逻辑,通过模块作用域"提升"到了模块层面,使得所有组件调用 Hook 时,复用同一个 useState 的结果。

2实战示例:3 个递进实现

示例 1:基础版全局状态 Hook(共享用户信息)

// useGlobalUser.js(模块级全局状态Hook)
import { useState, useEffect, useCallback } from 'react';

// 模块作用域的变量:存储用户状态(闭包缓存)
let globalUserState = null;
// 存储所有订阅组件的更新函数(用于状态变更时通知组件重渲染)
let subscribers = new Set();

// 触发所有订阅组件重渲染
const notifySubscribers = () => {
  subscribers.forEach(update => update());
};

// 自定义Hook:供组件调用
export function useGlobalUser() {
  // 每个组件调用Hook时,创建一个本地的更新函数(触发组件重渲染)
  const [, forceUpdate] = useState({});

  // 组件挂载时订阅,卸载时取消订阅
  useEffect(() => {
    subscribers.add(forceUpdate);
    return () => {
      subscribers.delete(forceUpdate);
    };
  }, [forceUpdate]);

  // 登录:更新全局状态并通知所有订阅组件
  const login = useCallback((userInfo) => {
    globalUserState = { ...userInfo };
    notifySubscribers(); // 触发所有组件重渲染
  }, []);

  // 登出:重置全局状态
  const logout = useCallback(() => {
    globalUserState = null;
    notifySubscribers();
  }, []);

  // 返回全局状态和操作方法
  return {
    user: globalUserState,
    login,
    logout,
    isLogin: !!globalUserState
  };
}
// Header组件
import { useGlobalUser } from './useGlobalUser';

export function Header() {
  const { user, isLogin, logout } = useGlobalUser();
  console.log('Header组件渲染:', user); // 验证是否共享状态

  return (
    <div>
      <h1>网站头部</h1>
      {isLogin ? (
        <div>
          欢迎 {user.name} <button onClick={logout}>登出</button>
        </div>
      ) : (
        <div>未登录</div>
      )}
    </div>
  );
}

// Login组件
import { useGlobalUser } from './useGlobalUser';

export function Login() {
  const { login } = useGlobalUser();

  const handleLogin = () => {
    // 模拟登录接口返回的用户信息
    login({ id: 1, name: '张三', avatar: 'xxx.png' });
  };

  return (
    <div>
      <button onClick={handleLogin}>模拟登录</button>
    </div>
  );
}

// 主页组件
import { Header } from './Header';
import { Login } from './Login';

export function Home() {
  return (
    <div>
      <Header />
      <Login />
    </div>
  );
}
✅ 效果验证:初始 Header 显示"未登录";点击"模拟登录"后 Login 调用 login 更新全局状态,notifySubscribers 触发 Header 重渲染 → 显示"欢迎张三"。Header 和 Login 通过 useGlobalUser 共享同一个 user 状态。

示例 2:优化版——用 useState 替代模块变量(更符合 React 规范)

// useGlobalUser优化版.js
import { useState, useCallback, useEffect } from 'react';

// 模块作用域:存储全局状态的Hook实例(闭包缓存)
let globalHook = null;

// 全局状态的核心Hook(仅初始化一次)
function createGlobalUserHook() {
  const [user, setUser] = useState(null);

  const login = useCallback((userInfo) => {
    setUser({ ...userInfo });
  }, []);

  const logout = useCallback(() => {
    setUser(null);
  }, []);

  return {
    user,
    login,
    logout,
    isLogin: !!user,
    // 暴露setUser供订阅逻辑使用(或直接用user作为依赖)
    subscribe: (callback) => {
      // 这里利用React的状态更新机制,无需手动维护订阅者
      // 组件通过useEffect依赖user,自动触发重渲染
    }
  };
}

export function useGlobalUser() {
  // 模块初始化时,创建全局Hook实例(仅执行一次)
  if (!globalHook) {
    globalHook = createGlobalUserHook();
  }

  // 返回全局状态和方法(所有组件共享同一个实例)
  return globalHook;
}
✅ 优化点:用 useState 替代模块变量,利用 React 的状态管理机制,无需手动维护订阅者;globalHook 在模块作用域中仅初始化一次,所有组件调用时返回同一个实例,状态完全共享。

示例 3:进阶版——支持多状态、批量更新(类似简易 Context)

// useGlobalState.js(通用全局状态Hook)
import { useState, useCallback, useEffect } from 'react';

// 模块作用域:存储所有全局状态(key-value形式)
const globalStates = new Map();
// 存储每个状态的订阅者
const subscribers = new Map();

// 创建或获取全局状态
function getOrCreateGlobalState(key, initialValue) {
  // 如果状态已存在,直接返回
  if (globalStates.has(key)) {
    return globalStates.get(key);
  }

  // 新建状态
  const [state, setState] = useState(initialValue);

  // 批量更新(类似setState的函数式更新)
  const setGlobalState = useCallback((updater) => {
    setState(prev => {
      const nextState = typeof updater === 'function' ? updater(prev) : updater;
      // 通知该状态的所有订阅者
      if (subscribers.has(key)) {
        subscribers.get(key).forEach(update => update(nextState));
      }
      return nextState;
    });
  }, [key]);

  const globalState = { state, setGlobalState };
  globalStates.set(key, globalState);
  subscribers.set(key, new Set());

  return globalState;
}

// 通用全局Hook:支持传入key和初始值
export function useGlobalState(key, initialValue) {
  const { state, setGlobalState } = getOrCreateGlobalState(key, initialValue);

  // 组件本地更新函数(触发组件重渲染)
  const [, forceUpdate] = useState(state);

  // 订阅状态变更:状态更新时触发组件重渲染
  useEffect(() => {
    const subs = subscribers.get(key);
    const update = (nextState) => forceUpdate(nextState);
    subs.add(update);

    // 初始触发一次渲染(同步初始状态)
    forceUpdate(state);

    return () => subs.delete(update);
  }, [key, state]);

  return [state, setGlobalState];
}
// 组件1:使用用户状态
const [user, setUser] = useGlobalState('user', null);

// 组件2:使用主题状态
const [theme, setTheme] = useGlobalState('theme', 'light');

// 组件3:更新全局状态
const handleChangeTheme = () => {
  setTheme(prev => prev === 'light' ? 'dark' : 'light');
};

const handleLogin = () => {
  setUser({ id: 1, name: '李四' });
};
📌 进阶特性:支持多状态隔离(通过 key 区分);支持函数式更新(批量修改状态);组件仅订阅自己关注的状态,性能更优。

3关键疑问:每次引入 state 会重新执行吗?

3.1 结论:不会重新初始化,但会重新执行 Hook 逻辑

3.2 代码验证:打印日志看执行次数

// useGlobalUser.js
import { useState, useCallback } from 'react';

let globalHook = null;
console.log('模块初始化:执行一次'); // 仅打印一次

function createGlobalUserHook() {
  console.log('创建全局状态:仅执行一次'); // 仅打印一次
  const [user, setUser] = useState(null);

  // ... 其余逻辑
}

export function useGlobalUser() {
  console.log('调用useGlobalUser:组件渲染时执行'); // 组件每次渲染都会打印

  if (!globalHook) {
    globalHook = createGlobalUserHook();
  }

  return globalHook;
}
时机日志输出
应用启动时模块初始化:执行一次
第一个组件(Header)挂载调用useGlobalUser:组件渲染时执行 → 创建全局状态:仅执行一次
第二个组件(Login)挂载调用useGlobalUser:组件渲染时执行(无"创建全局状态"日志)
组件重渲染(如登录后)两个组件都打印"调用useGlobalUser"(仍无"创建全局状态"日志)
⚠️ 注意:模块初始化和全局状态创建仅执行一次(state 不会重新初始化);Hook 函数本身会在组件每次渲染时执行,但仅读取缓存的状态,不会重复创建。

4适用场景与局限性 & 总结

4.1 适用场景

4.2 局限性

局限说明
不支持服务端渲染(SSR)模块作用域状态在 SSR 中被所有请求共享,导致状态污染
状态更新缺乏可追踪性没有 Redux DevTools 等工具支持,难以调试状态变更
不支持复杂状态逻辑异步状态流、状态依赖、批量更新优化等需手动实现,成本较高
组件卸载后状态不自动清理模块作用域状态一直存在直到应用刷新(可能内存泄漏)
不支持状态切片隔离多团队协作时容易出现状态 key 冲突(需手动规范命名)
✅ 总结:这种方案是一把"双刃剑"——它轻量化、易实现,适合简单场景;但缺乏高级特性和调试能力,不适合复杂应用。按项目规模选择:小型应用/简单状态用自定义 Hook中型应用/需要调试用 Zustand、Jotai(本质也是基于自定义 Hook + 闭包实现);大型应用/复杂状态流用 Redux、Redux Toolkit 或 Recoil