首页 / 文章 / 学习 React Hook:useState、useEffect、useReducer 以及自定义 Hook

学习 React Hook:useState、useEffect、useReducer 以及自定义 Hook

了解如何在 TypeScript 中正确使用 useState、useEffect、useReducer 以及自定义钩子,同时掌握何时选择 TypeScript 而非普通 JavaScript 才能带来实际好处。

1342 词

在编译时捕获类型错误可以避免漫长的调试过程,这些调试工作往往要持续到深夜。正因如此,将 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);

但如果初始值为nullundefined,情况就会发生变化——此时就需要手动标注类型,而不能依赖自动推断:

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 的约束机制都能使用,选择哪种语言主要取决于项目的生命周期长短以及协作程度。

相关阅读