学习 React Hook:useState、useEffect、useReducer 以及自定义 Hook
了解如何在 TypeScript 中正确使用 useState、useEffect、useReducer 以及自定义钩子,同时掌握何时选择 TypeScript 而非普通 JavaScript 才能带来实际好处。
在编译时捕获类型错误可以避免漫长的调试过程,这些调试工作往往要持续到深夜。正因如此,将 React 与 TypeScript 结合使用的团队会将带类型的钩子视为基本要求而非额外福利。掌握如何为状态、效应、还原器以及自定义钩子添加类型标注——并了解何时使用 TypeScript 才是正确选择——都是构建可靠 React 应用所需实用技能的两方面。
为何类型安全应融入你的钩子中
React Hooks 已经为开发者提供了更简洁、更具组合性的组件结构方式。TypeScript 在此基础上增加了安全层,使得错误能在编写代码时就被发现,而不会等到用户在实际使用中遇到故障界面之后才暴露出来。
从一开始,Hooks就被设计为让可复用的逻辑具有可预测性——Dan Abramov在讨论React的设计理念时也提到了这一点。TypeScript的贡献在于将这种可预测性明确化并加以验证,而非让开发者只能凭记忆来把握。
正确地为useState添加类型
在大多数日常情况下,TypeScript无需任何帮助就能自行推断出状态的类型:
// inferred as boolean, no annotation needed
const [isLoading, setIsLoading] = useState(false);
但如果初始值为null或undefined,情况就会发生变化——此时就需要手动标注类型,而不能依赖自动推断:
interface UserProfile {
id: string;
name: string;
avatarUrl: string;
}
const [user, setUser] = useState<UserProfile | null>(null);
这里一个有用的习惯是,不要为了让错误信息消失而随意使用泛型类型。这样做会彻底抵消你原本想从TypeScript那里获得的益处。
为useEffect添加类型注解:比想象中更简单
泛型并不真正适用于useEffect,但你仍需谨慎处理依赖数组和清理逻辑:
useEffect(() => {
const controller = new AbortController();
const fetchUser = async () => {
const res = await fetch(`/api/users/${userId}`, {
signal: controller.signal,
});
const data: UserProfile = await res.json();
setUser(data);
};
fetchUser();
return () => controller.abort(); // cleanup on unmount
}, [userId]);
为更复杂的状态使用useReducer添加类型注解
正是在这个钩子中,TypeScript的优势才得以充分体现:
type CartAction =
| { type: 'ADD_ITEM'; payload: CartItem }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'CLEAR_CART' };
function cartReducer(state: CartItem[], action: CartAction): CartItem[] {
switch (action.type) {
case 'ADD_ITEM':
return [...state, action.payload];
case 'REMOVE_ITEM':
return state.filter((item) => item.id !== action.payload);
case 'CLEAR_CART':
return [];
default:
return state;
}
}
通过这种方式对动作类型进行建模后,发送无效数据会立即导致编译时错误,让你能及时发现问题,而不会在运行时才意外察觉。
使用泛型编写可复用的自定义钩子
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = window.localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
由于该钩子是泛型的,因此可以在代码库的任何地方重复使用——无论是处理字符串、对象还是数组——同时还能为传递通过的数据保持完全的类型精确性。
简而言之
- 让 TypeScript 自动推断简单状态,但当值可能为 null 或 undefined 时需明确处理。
- 将 useReducer 的动作建模为区分联合类型。
- 在自定义钩子中使用泛型,以便其在不同数据类型间重复使用。
- 将
any视为一种快捷方式,但它带来的后续代价往往高于其节省的成本。
纯 JavaScript 与 TypeScript 的选择
一旦理解了类型化钩子带来的安全性优势,一个更广泛的问题自然会出现:是否每个项目都应使用 TypeScript,还是说这有时属于过度设计?很长一段时间里,这个问题都被视为非此即彼的选择,无论选择哪一方都需接受相应的权衡。
过去,若要使用 TypeScript,就必须处理构建流程、ts-node 等工具以及大量配置文件,才能让脚本运行。如今,Node.js 等运行时已支持原生类型剥离功能,这些繁琐问题大多已消失,编写普通 JavaScript 与 TypeScript 之间的界限也变得比以往更为模糊。
了解每种选择对日常工作流程的影响,有助于更轻松地决定哪种方式最适合当前项目。
为什么普通 JavaScript 仍有其价值
JavaScript 是浏览器和 Node.js 原生支持的语言。你编写文件后直接运行,它就能立即执行,无需经过编译器处理,也无需额外声明任何内容。
- 即时原型开发:在测试想法、快速编写脚本或构建小型最小可行产品时,JavaScript能让你的开发速度与思维速度保持一致。
- 无需额外配置:无需编写
tsconfig.json文件,也无需进行类型检查就能确认函数是否能按预期工作。 - 减少脑力负担:你可以将注意力集中在程序的实际逻辑上,而非类型声明或编译器报错上。
不过,当JavaScript代码量超过几千行时,其缺点就会显现出来。此时进行重构会变得风险很高——要在二十个文件中重命名某个属性,就得依赖全局查找替换功能,并且还得担心程序运行时会出现未知故障。
为何项目规模扩大后TypeScript更值得使用
TypeScript在JavaScript之上添加了静态类型系统,就像一个自动审查器,在你编码时就能指出问题——例如,一旦你试图将字符串传递给期望接收数字的函数,它就会立即发出警告。
- 始终准确的文档:类型同时也起到了动态文档的作用。接口能让你直接了解对象应有的确切结构,无需遍历多个实现文件。
- 更安全的代码重构:如果你修改了数据模型或数据库架构,编译器会指出代码库中所有需要更新的位置。
- 更好的编辑器支持:VS Code或Cursor等工具依靠TypeScript的语言服务器,在你编写代码时提供快速的自动补全和内联错误高亮功能。
从历史上看,这种权衡意味着要支付真正的“工具成本”——配置编译器、源码映射文件以及封装脚本,为原本简单的流程增加了额外的步骤。
为何这种权衡不再适用
过去“TypeScript 的设置太麻烦”这一抱怨已不再那么有道理。得益于类型剥离技术,当前版本的 Node.js 可以直接运行 TypeScript 文件,无需额外的构建步骤。
在后台,运行时只会移除类型注解和接口声明,留下纯 JavaScript 代码以便立即执行。这样既能保持静态类型的开发时安全性,又无需承担以往的设置开销。
选择合适的工具
对于一次性脚本、简单的自动化任务,或是仅用于了解网页工作原理的学习项目,纯 JavaScript 依然是更合适的选择。省去额外的结构能让流程更加轻量且有趣。
而对于其他几乎所有场景——团队代码库、包含多个交互数据模型的应用程序,或是任何预计需要维护超过一个月的项目——TypeScript 所需的少量配置工作是值得的。由于现代运行时无论哪种方式都能流畅地执行代码,因此实际上无需在灵活性与安全性之间做选择:JavaScript 的简洁性与 TypeScript 的约束机制都能使用,选择哪种语言主要取决于项目的生命周期长短以及协作程度。
相关阅读
- TypeScript 6.0的破坏性默认值:实用迁移指南 — 了解TypeScript 6.0中有哪九个编译器默认值发生了变化,如何为2026年配置tsconfig,以及如何让代码库准备好迎接基于Go的TypeScript 7。
- 2026年的全栈JavaScript默认值:TypeScript、RSC及更多 — 阐述了为何在2026年,TypeScript、React Server Components以及更精简的状态管理方式已成为JavaScript团队的标准生产环境技术栈。