React + TypeScript 入门:从组件类型到数据流设计

引言

如果你刚接触 React 和 TypeScript 的组合,可能会有这样的感受:JavaScript 还没写利索,怎么又多了一套类型语法?别急,今天我们从一个非常具体的场景出发------在 React 中管理一个"用户名" ,一步一步把 TypeScript 在 React 项目中的用法搞清楚。

这篇文章的目标读者是:对 React 有基本了解(知道组件、props、state 是什么),但对 TypeScript 还很陌生的人。我们会从最基础的组件类型定义讲起,逐步深入到父子组件的数据流设计。全程用代码说话,不堆砌概念。

一、从一个简单的 Hello 组件开始

假设我们要写一个 React 组件,它的任务很简单:接收一个用户名,然后显示一句问候语

在纯 JavaScript 中,我们可能会这样写:

tsx

javascript 复制代码
const HelloComponent = (props) => {
  return <h2>Hello {props.userName}</h2>;
};

这段代码能跑,但有一个问题:谁也不知道 props 里面到底有什么 。如果调用者传错了属性名,比如传了 username 而不是 userName,程序不会报错,只是页面显示"Hello undefined"------然后你得花时间去排查。

TypeScript 解决的就是这个问题。我们给 props 加上一份"说明书",明确告诉调用者:这个组件需要一个叫 userName 的属性,而且它的值必须是字符串

tsx

typescript 复制代码
import * as React from 'react';

// 用 interface 定义 Props 的"形状"
interface Props {
  userName: string;  // 冒号后面是类型,表示这个属性必须是字符串
}

// React.FC 是"Function Component"的缩写,是一个泛型类型
// 把 Props 传进去,就等于告诉 TypeScript:这个组件的 props 必须符合 Props 接口
const HelloComponent: React.FC<Props> = (props) => {
  return <h2>Hello {props.userName}</h2>;
};

export default HelloComponent;

这里有两个关键点需要展开说一下。

1.1 interface 是什么?

interface 是 TypeScript 中用来描述对象结构的工具。你可以把它理解成一份"合同"或"蓝图"------它规定了某个对象必须具备哪些属性,以及这些属性分别是什么类型。

在上面的例子里,Props 接口规定:任何被当作 HelloComponent 的 props 使用的对象,必须有一个名为 userName 的属性,且该属性的值必须是字符串

如果调用者这样使用:

tsx

ini 复制代码
<HelloComponent userName="张三" />  // ✅ 正确
<HelloComponent user="张三" />      // ❌ 报错:缺少 userName
<HelloComponent userName={123} />   // ❌ 报错:userName 应该是字符串

TypeScript 会在你编写代码时就给出错误提示,而不是等到页面运行时才发现问题

1.2 React.FC 是什么?

React.FC 是 TypeScript 为 React 函数组件提供的类型定义 。它是一个泛型(generic),可以接收一个类型参数------也就是你的 Props 接口。

使用 React.FC<Props> 的好处是:

  • 它会自动为你的组件加上 children 属性的类型定义(虽然我们这里没用上)
  • 它明确了返回值类型是 React.ReactElement,让类型检查更完整

当然,你也可以不用 React.FC,直接这样写:

tsx

javascript 复制代码
const HelloComponent = (props: Props) => {
  return <h2>Hello {props.userName}</h2>;
};

两种写法都可行,但 React.FC 是更规范的做法,在团队项目中推荐使用

二、让用户修改名字:引入表单交互

光显示名字还不够,我们想让用户自己输入并修改名字 。这就需要引入表单元素(<input>)和事件处理。

先看一个最基础的版本:

tsx

typescript 复制代码
import * as React from 'react';

interface Props {
  editingName: string;                        // 当前正在编辑的名字
  onEditingNameUpdated: (newName: string) => void;  // 名字变化时的回调
  onNameUpdated: () => void;                  // 确认更新时的回调
  disabled: boolean;                          // 按钮是否禁用
}

const NameEditingComponent: React.FC<Props> = (props) => {
  const { editingName, onEditingNameUpdated, onNameUpdated, disabled } = props;

  // 处理输入框内容变化
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    onEditingNameUpdated(e.target.value);
  };

  // 处理"确认修改"按钮点击
  const onNameSubmit = () => {
    onNameUpdated();
  };

  return (
    <>
      <label>更新名字:</label>
      <input value={editingName} onChange={onChange} />
      <button disabled={disabled} onClick={onNameSubmit}>
        确认修改
      </button>
    </>
  );
};

export default NameEditingComponent;

这段代码的核心是把"数据"和"修改数据的操作"都通过 props 交给父组件管理 。子组件只负责展示和触发事件,不自己保存任何状态。这种模式叫作受控组件(controlled component)------输入框的值完全由父组件控制。

2.1 事件类型为什么要写那么长?

你可能注意到了 onChange 的参数类型写得很长:

tsx

css 复制代码
(e: React.ChangeEvent<HTMLInputElement>) => { ... }

React.ChangeEvent<HTMLInputElement> 是 React 提供的事件类型 ,它描述了"一个发生在 HTML 输入框上的变化事件"。这个类型包含了事件对象的所有信息,比如 e.target.value(输入框当前的内容)、e.target.name(输入框的名字)等等。

写这个类型的好处是:

  1. 编辑器会自动提示 :当你输入 e. 的时候,编辑器会列出所有可用属性和方法
  2. 防止拼写错误 :如果你把 e.target.value 写成 e.target.valule,TypeScript 会报错
  3. 代码即文档:任何人看到这个类型,都知道这个函数是用来处理输入框变化事件的

React 还提供了其他常用的事件类型

  • React.MouseEvent<HTMLButtonElement> ------ 按钮点击事件
  • React.FormEvent<HTMLFormElement> ------ 表单提交事件
  • React.KeyboardEvent<HTMLInputElement> ------ 键盘事件

2.2 这个组件的设计思路

这个 NameEditingComponent 的设计遵循了一个重要原则:数据向下流动,事件向上传递

  • 数据向下editingName 从父组件传入,子组件只负责展示
  • 事件向上 :用户输入时,子组件通过 onEditingNameUpdated 把新值"汇报"给父组件;用户点击按钮时,通过 onNameUpdated 通知父组件"可以正式更新了"

这样做的好处是数据流向清晰 。所有关于名字的数据都存放在父组件中,子组件只是一个"展示+交互"的壳。当项目变大、组件变多时,这种清晰的流向能极大降低调试难度

三、让组件自己管状态:另一种设计思路

上面的例子中,所有数据都由父组件管理。但有时候,让子组件自己管理一部分状态会让代码更简洁。来看另一种写法:

tsx

typescript 复制代码
import * as React from 'react';

interface Props {
  initialUserName: string;                 // 初始名字
  onNameUpdated: (newName: string) => void; // 确认更新时通知父组件
}

const NameEditComponent: React.FC<Props> = (props) => {
  // 用 useState 在组件内部维护一个"编辑中的名字"
  const [editingName, setEditingName] = React.useState(props.initialUserName);

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setEditingName(e.target.value);  // 更新内部状态
  };

  const onNameSubmit = () => {
    props.onNameUpdated(editingName);  // 把最终结果告诉父组件
  };

  return (
    <>
      <label>更新名字:</label>
      <input value={editingName} onChange={onChange} />
      <button onClick={onNameSubmit}>确认修改</button>
    </>
  );
};

export default NameEditComponent;

3.1 useState 的类型

useState 是 React 提供的 Hook,用来在函数组件中创建和管理状态。

tsx

ini 复制代码
const [editingName, setEditingName] = React.useState(props.initialUserName);

这行代码做了三件事:

  1. 创建一个名叫 editingName 的状态变量,初始值为 props.initialUserName
  2. 创建一个名叫 setEditingName 的函数,用来修改 editingName
  3. setEditingName 被调用时,React 会重新渲染组件,并更新界面

TypeScript 在这里发挥了作用:它会根据初始值自动推断类型 。因为 props.initialUserName 是字符串,TypeScript 就知道 editingName 也应该是字符串,setEditingName 只能接收字符串参数

你也可以显式指定类型:

tsx

c 复制代码
const [editingName, setEditingName] = React.useState<string>(props.initialUserName);

两种写法都可以,但在初始值足够明确的情况下,让 TypeScript 自动推断更简洁。

3.2 两种设计模式对比

现在我们有两种设计思路

第一种(受控组件) 第二种(内部状态)
数据存在哪 父组件 子组件内部
谁负责更新数据 父组件 子组件自己
父组件需要传什么 editingName + 两个回调 initialUserName + 一个回调
适用场景 需要父组件实时感知变化 只需最终结果,中间过程不重要

第一种方式更"重",父组件需要管理更多状态,但控制力更强------父组件可以随时知道用户输入了什么。

第二种方式更"轻",父组件只需要传初始值和最终回调,中间过程完全由子组件自己打理。适合那些"只需要最终结果"的场景。

没有哪种更好,只有哪种更合适。根据实际需求选择就行。

四、把它们组合起来:一个完整的 App

现在我们把所有组件组合成一个完整的应用。这个应用的功能是:

  1. 页面加载后,从"服务器"(这里用 setTimeout 模拟)获取用户名
  2. 展示当前用户名
  3. 用户可以通过输入框修改名字,点击确认后正式更新

tsx

typescript 复制代码
import * as React from 'react';
import HelloComponent from './components/Hello';
import NameEditingComponent from './components/NameEditingComponent';

const App = () => {
  // 正式的名字
  const [name, setName] = React.useState<string>('defaultUserName');
  // 编辑中的名字(还没正式确认)
  const [editingName, setEditingName] = React.useState('defaultUserName');

  // 模拟从服务器加载用户名
  const loadUsername = () => {
    setTimeout(() => {
      setName('name from async call');
      setEditingName('name from async call');
    }, 2000);
  };

  // 组件首次渲染后执行
  React.useEffect(() => {
    loadUsername();
  }, []);

  // 用户点击"确认修改"时调用
  const setUserNameState = () => {
    setName(editingName);
  };

  return (
    <>
      当前名字:{name}
      <HelloComponent userName={editingName} />
      <NameEditingComponent
        editingName={editingName}
        onNameUpdated={setUserNameState}
        onEditingNameUpdated={setEditingName}
        disabled={editingName === '' || editingName === name}
      />
    </>
  );
};

export default App;

4.1 useEffect 是做什么的?

useEffect 是 React 的另一个 Hook,用来处理副作用 (side effect)。什么是副作用?简单说,就是不属于"根据数据渲染 UI"这个主流程的操作,比如:

  • 从服务器获取数据
  • 操作 DOM(比如修改页面标题)
  • 设置定时器

tsx

scss 复制代码
React.useEffect(() => {
  loadUsername();
}, []);

这个写法表示:在组件第一次渲染完成后执行 loadUsername 。第二个参数 [] 是一个空数组,表示这个效果不依赖任何外部变量,所以只执行一次。

如果不传第二个参数,useEffect 会在每次渲染后都执行,这通常不是我们想要的。

4.2 状态更新的流程

整个应用的数据流是这样的:

  1. 初始化nameeditingName 都被设为 'defaultUserName'
  2. 加载数据 :组件渲染后,useEffect 触发 loadUsername,2 秒后把两个状态都更新为 'name from async call'
  3. 用户编辑 :用户在输入框中打字,触发 onChange,调用 setEditingName 更新编辑中的名字(此时正式名字 name 不变)
  4. 确认修改 :用户点击按钮,触发 onNameSubmit,调用 setUserNameState,把 name 更新为 editingName 的值
  5. 按钮禁用 :当 editingName 为空字符串,或者 editingNamename 相同时,按钮不可用------避免无效操作

这个流程中,TypeScript 确保了每一步的数据类型都是正确的:setName 只能接收字符串,setEditingName 也只能接收字符串,任何类型错误都会在编译时被捕获

五、总结

通过这个"用户名管理"的小例子,我们覆盖了 React + TypeScript 开发中的几个核心知识点:

  1. 用 interface 定义 Props 类型:给组件参数加上"说明书",让调用者明确知道需要传什么
  2. React.FC 泛型类型 :为函数组件提供完整的类型约束
  3. 事件类型React.ChangeEventReact.MouseEvent 等,让事件处理函数类型安全
  4. useState 的类型推断 :根据初始值自动推导状态类型,也可以显式指定
  5. useEffect 处理副作用:在组件渲染后执行异步操作
  6. 受控组件 vs 内部状态:两种数据管理模式的取舍
  7. 单向数据流 :数据从父组件流向子组件,事件从子组件流回父组件

TypeScript 给 React 带来的最大价值,不是让你多写几行类型代码,而是让你在写代码的时候就能发现潜在的问题 ,而不是等到页面报错再去排查。对于初学者来说,这可能会多花一些学习成本,但一旦习惯了这种"先定义类型再写逻辑"的方式,你会发现自己写的代码更自信、更少出错。

希望这篇文章能帮你迈出 React + TypeScript 的第一步。

相关推荐
烬羽1 小时前
React 状态归属:两个版本的用户名编辑器,告诉你 state 该放哪
react.js·typescript·前端框架
名字还没想好☜2 小时前
React createPortal 实战:模态框逃出 overflow:hidden、事件冒泡与焦点管理
前端·javascript·react.js·ecmascript·react·createportal
GitLqr20 小时前
提升 Web 开发效率:我常用的 10 个 TypeScript 实战技巧
vue.js·react.js·typescript
无人生还1 天前
从 Vue3 到 React · 快速上手系列第 6 篇:状态管理 useState 与 useReducer
前端·vue.js·react.js
张元清1 天前
React usePrevious Hook:追踪上一次的 State 和 Props(2026)
javascript·react.js
LaughingZhu1 天前
Product Hunt 每日热榜 | 2026-07-28
前端·神经网络·react.js·搜索引擎·前端框架
Highcharts.js1 天前
教程:基于 React + Highcharts 构建一个单页应用程序、按需数据拉取与图表渲染
开发语言·前端·数据结构·react.js·前端框架·highcharts·页面应用
To_OC1 天前
我被 useState 坑了两次之后,终于把它的脾气摸透了
前端·javascript·react.js
Luu.ྀ2 天前
React详细笔记
javascript·笔记·react.js