用Constate实现高效状态管理的实战经验分享
先写个计数器,看看这玩意儿咋用
别整那些虚的,上来就干。我第一次用 Constate 的时候,就是从最简单的计数器开始的,结果发现比 React Context 简单太多了。
直接上代码:
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 了,又是被产品经理追着问的一天。

暂无评论