做企业级React项目不用TS,后期重构直接崩溃。
很多人只会简单写React.FC,但props泛型、合成事件、useEffect副作用、前端持久化存储全是模糊点。
读完这篇你能掌握:
- React.FC、interface/type、泛型完整类型约束写法
- 父子组件单向数据流标准通信方案
- React合成事件TS类型规范(ChangeEvent)
- useEffect生命周期与副作用实战场景
- localStorage与IndexedDB选型、代码差异、适用场景
- 企业级无状态组件最佳实践
全文代码可直接复制运行,踩坑点全部标注,新手也能看懂。
一、为什么企业项目强制 React + TypeScript
React本身源码就是TS编写,搭配TS是大型项目标配,核心收益3点:
- 静态类型校验:开发阶段直接报类型错误,不用等到页面运行才崩溃
- 类型即文档:组件props、入参、返回值自带提示,团队协作零沟通成本
- 完善高级语法:泛型、接口、类型别名,解决JS弱类型带来的各种隐性bug
无TS项目痛点: 传参拼写错误、事件对象类型混乱、复杂对象无约束、重构时牵一发而动全身。
二、React函数组件核心类型:React.FC 完整解析
2.1 React.FC 底层定义
// React源码简化定义
type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
(props: P & { children?: ReactNode }): ReactNode;
}
关键点:
FC是类型别名,接收泛型P约束props- 组件返回值强制为
ReactNode,不能返回普通字符串/数字 - 自带可选
children属性,不用手动声明
2.2 基础Hello组件实战(Props类型约束)
// 用interface定义组件入参规范
interface HelloProps {
name: string;
age?: number; // 可选参数
}
// FC泛型传递Props类型约束
const Hello: React.FC<HelloProps> = ({ name, age }) => {
return (
<div>
<p>你好,{name},今年{age ?? "未知"}岁</p>
</div>
);
};
// 使用
<Hello name="张三" age={22} />
2.3 interface vs type 怎么选(高频踩坑)
| 写法 | 适用场景 |
|---|---|
interface | 定义组件props、对象结构、需要扩展属性 |
type | 联合类型、交叉类型、简单类型别名 |
示例区分:
// interface:支持继承扩展
interface User {
id: number;
name: string;
}
interface UserInfo extends User {
address: string;
}
// type:仅做别名,不支持继承
type UserType = {
id: number;
name: string;
}
三、React合成事件TS类型(表单开发必用)
原生事件写法类型模糊,React提供内置事件泛型React.ChangeEvent,精准绑定DOM元素类型。
3.1 输入框受控组件标准写法
import { useState } from 'react';
const InputDemo: React.FC = () => {
const [editingName, setEditingName] = useState("");
// 泛型指定HTMLInputElement,完整拿到输入框DOM类型
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditingName(e.target.value);
};
return (
<input value={editingName} onChange={handleChange} placeholder="请输入名称" />
);
};
踩坑提醒:
不要直接写(e) => void,丢失DOM元素类型,无法获取target完整属性。
3.2 自定义事件父子通信类型规范
子组件抛出自定义事件,父组件接收,统一约束回调函数类型:
// 子组件Props
interface ChildProps {
// 自定义回调,参数为输入字符串
onChangeName: (val: string) => void;
}
const ChildInput: React.FC<ChildProps> = ({ onChangeName }) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// 向上抛出值
onChangeName(e.target.value);
};
return <input onChange={handleChange} />;
};
// 父组件使用
const Parent = () => {
const handleUpdate = (name: string) => {
console.log("子组件传过来的值:", name);
};
return <ChildInput onChangeName={handleUpdate} />;
};
四、单向数据流:组件状态分层3个迭代版本(企业标准)
核心原则:父组件持有状态,子组件仅展示,状态通过props下发,修改方法通过自定义事件上抛,也就是UI = fn(props)。
版本1:原始写法(状态分散,可读性差)
子组件私有状态,父子各自维护一份数据,数据不同步,多人协作极易出bug。
// 子组件自己存editingName,父组件无法同步
const Child = () => {
const [editingName, setEditingName] = useState("");
const change = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditingName(e.target.value);
};
return <input value={editingName} onChange={change} />;
};
版本2:折中方案(子私有状态,提交同步父)
子组件编辑时操作本地状态,仅提交时同步父组件,适合临时草稿场景。
版本3:最优方案(状态提升,纯展示子组件)
所有状态统一放到父组件,子组件无任何state,纯受控组件,性能更好、数据统一。
// 父组件:唯一状态持有者
const Parent = () => {
const [editingName, setEditingName] = useState("");
const handleChange = (val: string) => {
setEditingName(val);
};
return <ChildInput value={editingName} onChange={handleChange} />;
};
// 子组件:无状态,只接收props渲染
interface ChildInputProps {
value: string;
onChange: (v: string) => void;
}
const ChildInput: React.FC<ChildInputProps> = ({ value, onChange }) => {
const inputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};
return <input value={value} onChange={inputChange} />;
};
优势:
- 全局数据唯一源头,不会出现数据不一致
- 子组件无渲染冗余,性能更高
- 全局状态统一管理,方便持久化存储
五、useEffect副作用钩子:替代全套生命周期
5.1 useEffect对应三大生命周期
- 挂载后mounted:依赖数组传
[],仅页面初始化执行一次 - 更新后updated:依赖数组填入监听变量,变量变化执行
- 卸载前unmounted:函数内部返回清理函数,组件销毁执行
5.2 接口请求标准实战(最常用场景)
页面挂载后请求后端接口,拿到数据渲染列表:
import { useState, useEffect } from 'react';
interface UserItem {
id: number;
name: string;
}
const UserList: React.FC = () => {
const [userList, setUserList] = useState<UserItem[]>([]);
useEffect(() => {
// 模拟接口请求
const fetchUser = async () => {
const res = await fetch("/api/user");
const data = await res.json();
setUserList(data);
};
fetchUser();
// 卸载清理:取消请求,防止组件销毁后setState报错
return () => {
console.log("组件卸载,清理副作用");
};
}, []); // 空依赖,仅挂载执行一次
return (
<div>
{userList.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
};
踩坑提醒: 依赖数组必须补齐所有内部使用的变量,否则会出现闭包陷阱。
六、前端本地持久化:localStorage vs IndexedDB 完整对比
6.1 localStorage 基础使用代码
限制:仅支持字符串存储,容量约5MB,同步API,阻塞主线程。
// 写入对象必须序列化
const saveUser = (user: UserItem) => {
localStorage.setItem("user", JSON.stringify(user));
};
// 读取需要反序列化
const getUser = (): UserItem | null => {
const str = localStorage.getItem("user");
if (!str) return null;
return JSON.parse(str);
};
// 删除
localStorage.removeItem("user");
// 清空全部
localStorage.clear();
适用场景:少量配置、token、主题、简单偏好设置。
6.2 IndexedDB 核心特点
- 异步API,不会阻塞页面渲染
- 大容量存储,支持Blob、二进制、对象直接存储,无需JSON序列化
- 支持数据表、索引、条件筛选、分页查询
- 适合离线缓存、大量列表、本地文件存储
原生API写法繁琐,项目中统一使用Dexie.js封装简化开发。
6.3 两者核心区别总结
| 维度 | localStorage | IndexedDB |
|---|---|---|
| 读写模式 | 同步,阻塞UI | 异步Promise,不卡顿页面 |
| 存储容量 | 约5MB | 几十MB起,受磁盘配额限制 |
| 数据类型 | 仅字符串,对象需序列化 | 原生支持对象、Blob、ArrayBuffer |
| 查询能力 | 仅按键取值,无筛选 | 支持索引、条件、分页查询 |
| 上手难度 | 极简,一行调用 | 原生复杂,需封装库 |
选型建议
- 几十KB简单配置 → localStorage
- 大量数据、离线页面、存储图片文件、需要筛选数据 → IndexedDB
七、全文核心踩坑汇总(开发高频问题)
- 使用
React.FC忘记泛型,props无类型约束,编辑器无提示 - 合成事件不写
React.ChangeEvent<HTMLInputElement>,丢失DOM类型 - 父子组件状态不提升,多处维护同一份数据,造成数据不同步
- useEffect依赖数组漏写变量,出现闭包旧值bug
- localStorage直接存对象,忘记
JSON.stringify,读取后变成[object Object] - 大批量数据使用localStorage存储,同步读写导致页面严重卡顿
八、完整实战总结
- React企业级项目优先搭配TS,用
React.FC+interface约束组件props,类型自文档化 - 表单事件统一使用React内置合成事件泛型,规范类型、减少报错
- 严格遵循单向数据流,状态统一提升至父组件,子组件做纯UI渲染
- useEffect替代class生命周期,处理接口请求、定时器等副作用,记得清理卸载资源
- 小体量配置用localStorage,大批量离线数据、文件存储选用IndexedDB