用Constate实现高效状态管理的实战经验分享

上官美玲 框架 阅读 2,085
赞 16 收藏
二维码
手机扫码查看
反馈

先写个计数器,看看这玩意儿咋用

别整那些虚的,上来就干。我第一次用 Constate 的时候,就是从最简单的计数器开始的,结果发现比 React Context 简单太多了。

用Constate实现高效状态管理的实战经验分享

直接上代码:

import { createContainer } from 'constate';

function useCounter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(prev => prev + 1);
  const decrement = () => setCount(prev => prev - 1);
  return { count, increment, decrement };
}

const { Provider, useContainer } = createContainer(useCounter);

function CounterDisplay() {
  const { count } = useContainer();
  return <div>当前数字:{count}</div>;
}

function CounterButtons() {
  const { increment, decrement } = useContainer();
  return (
    <div>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  );
}

function App() {
  return (
    <Provider>
      <CounterDisplay />
      <CounterButtons />
    </Provider>
  );
}

看到没?就这几个函数拆开,状态自动共享。我之前还傻乎乎地用 useContext + useReducer 搞一堆 action type,折腾半天。Constate 这种写法,逻辑清晰,组件复用也方便。

这个场景最好用:表单联动

真正让我觉得“这工具买账了”的,是处理一个带联动的城市选择器——省、市、区三级联动。之前我用自定义 Hook 拼,但父子传参太乱。改用 Constate 后,整个流程清爽多了。

import { createContainer } from 'constate';
import { useState, useEffect } from 'react';

function useFormState() {
  const [province, setProvince] = useState('');
  const [city, setCity] = useState('');
  const [district, setDistrict] = useState('');
  const [cities, setCities] = useState([]);
  const [districts, setDistricts] = useState([]);

  // 模拟获取城市列表
  useEffect(() => {
    if (!province) return;
    fetch(https://jztheme.com/api/cities?province=${province})
      .then(res => res.json())
      .then(data => setCities(data));
  }, [province]);

  // 获取区县
  useEffect(() => {
    if (!city) return;
    fetch(https://jztheme.com/api/districts?city=${city})
      .then(res => res.json())
      .then(data => setDistricts(data));
  }, [city]);

  const resetCityAndDistrict = () => {
    setCity('');
    setDistrict('');
    setCities([]);
    setDistricts([]);
  };

  const handleProvinceChange = (value) => {
    setProvince(value);
    resetCityAndDistrict();
  };

  const handleCityChange = (value) => {
    setCity(value);
    setDistrict('');
    setDistricts([]);
  };

  return {
    province,
    city,
    district,
    cities,
    districts,
    handleProvinceChange,
    handleCityChange,
    setDistrict
  };
}

const { Provider: FormProvider, useContainer: useForm } = createContainer(useFormState);

然后在组件里拆着用:

function ProvinceSelector() {
  const { province, handleProvinceChange } = useForm();
  return (
    <select value={province} onChange={e => handleProvinceChange(e.target.value)}>
      <option value="">请选择省份</option>
      <option value="zhejiang">浙江</option>
      <option value="jiangsu">江苏</option>
    </select>
  );
}

function CitySelector() {
  const { city, cities, handleCityChange } = useForm();
  return (
    <select value={city} onChange={e => handleCityChange(e.target.value)} disabled={!cities.length}>
      <option value="">请选择城市</option>
      {cities.map(c => <option key={c} value={c}>{c}</option>)}
    </select>
  );
}

重点来了:这三个组件完全独立,但我只用了一个 useForm() 就拿到所有状态和方法。没有层层传递,也没有 reducer action 写到吐。

踩坑提醒:这三点一定注意

  • 别忘了 Provider 包裹 —— 我有次调试半天发现子组件拿不到值,最后发现是某个路由页漏包了 FormProvider。Constate 不会报错,只是 useContainer() 返回 undefined,很容易懵逼。
  • 多个实例?小心内存泄漏 —— 如果你在列表里每个 item 都创建一个新的 createContainer 实例(比如通过闭包动态生成),那会出事。建议把容器提到外面,通过 props 传 id 区分状态,不然性能直接拉垮。
  • TS 类型推导有时候不准 —— 虽然 Constate 支持 TypeScript,但如果你的 Hook 返回值用了复杂的联合类型或者异步处理,useContainer 可能推不出类型。这时候建议手动加个类型断言:
interface FormState {
  province: string;
  city: string;
  district: string;
  cities: string[];
  districts: string[];
  handleProvinceChange: (v: string) => void;
  handleCityChange: (v: string) => void;
  setDistrict: (v: string) => void;
}

const { Provider, useContainer } = createContainer(useFormState);

// 强制类型
export function useForm() {
  const context = useContainer();
  if (!context) throw new Error('useForm 必须在 FormProvider 内使用');
  return context as FormState;
}

这样后面调用的时候就不会类型丢失了。亲测有效。

高级技巧:多个 Hook 共享同一个 Provider

你可能不知道,createContainer 其实可以接收多个 Hook,让一个 Provider 提供多种状态。这在复杂页面特别有用。

function useUserData() {
  const [user, setUser] = useState(null);
  const login = (username) => setUser({ username });
  const logout = () => setUser(null);
  return { user, login, logout };
}

function useTheme() {
  const [darkMode, setDarkMode] = useState(false);
  const toggleTheme = () => setDarkMode(prev => !prev);
  return { darkMode, toggleTheme };
}

// 一次性打包两个 Hook
const { Provider, useContainer } = createContainer((props) => ({
  user: useUserData(props),
  theme: useTheme(props)
}));

然后在组件里按需取用:

function Header() {
  const { user } = useContainer().user;
  const { darkMode, toggleTheme } = useContainer().theme;
  return (
    <header className={darkMode ? 'dark' : 'light'}>
      <span>你好,{user?.username || '游客'}</span>
      <button onClick={toggleTheme}>切换主题</button>
    </header>
  );
}

这种模式适合管理页面级的复合状态,比如后台系统里用户+权限+主题这种组合。比写一堆 Context 干净多了。

为啥我不再滥用 Redux 了

说句得罪人的话:很多项目根本不需要 Redux。尤其是中小型应用,状态就那么几个,非得搞个 store、reducer、action、thunk 中间件,纯属给自己找罪受。

Constate 的优势就在于“轻”:它不引入新概念,就是基于你 already know 的 useState 和自定义 Hook。而且它不会强制你做状态归一,你可以每个模块自己管自己的容器,想拆就拆,想合就合。

当然,如果你项目已经上了 RTK 或者 Zustand,也没必要换。但如果你刚开始搭项目,又不想被 Context 嵌套搞疯,Constate 是个非常务实的选择。

顺便提一嘴:Constate 官方已经不再维护了(GitHub 上写着 “deprecated”),但这不代表不能用。它的 API 极其稳定,核心代码就几百行,没有任何副作用,我在生产环境跑了两年多,没出过任何运行时问题。

真要出问题,我自己都能 fork 一份修。所以别被 “deprecated” 吓到,关键是看它解决的问题你现在是不是还需要。

总结:简单事就别搞复杂了

以上是我踩坑后的总结,希望对你有帮助。Constate 不是什么银弹,但它很好地解决了“中等复杂度状态共享”这个痛点。

我的建议是:当你发现自己开始写 useState + useCallback + useContext 组合拳,而且还要跨三四层组件传 props 的时候,就可以考虑上 Constate 了。

这个技巧的拓展用法还有很多,比如结合 SWR 做数据缓存容器、封装表单校验流程等等,后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。现在我要去修另一个 bug 了,又是被产品经理追着问的一天。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论