React + TypeScript 项目的完整工程规范,涵盖项目结构、组件设计、Hooks、路由、状态管理、API 层、错误处理、测试和性能优化。当用户在 React 项目中创建、修改组件或模块,涉及架构设计、代码编写时自动激活。
适用于使用 React + TypeScript 的仓库。
以下为中大型 React 项目的业界最佳实践结构,按项目实际情况裁剪:
src/
├── app/ # 应用入口与全局配置
│ ├── App.tsx # 根组件(Provider 组合)
│ ├── routes.tsx # 路由配置
│ └── providers.tsx # 全局 Provider 组装
│
├── pages/ # 页面组件(与路由一一对应)
│ ├── Dashboard/
│ │ ├── DashboardPage.tsx
│ │ ├── components/ # 页面私有组件
│ │ └── hooks/ # 页面私有 hooks
│ ├── UserList/
│ └── Settings/
│
├── layouts/ # 布局组件
│ ├── MainLayout.tsx # 主布局(侧边栏 + 顶栏 + 内容区)
│ ├── AuthLayout.tsx # 登录/注册页布局
│ └── BlankLayout.tsx # 空白布局(错误页等)
│
├── features/ # 功能模块(按业务领域划分)
│ ├── auth/
│ │ ├── components/ # 模块组件
│ │ ├── hooks/ # 模块 hooks
│ │ ├── api.ts # 模块 API 调用
│ │ ├── types.ts # 模块类型定义
│ │ ├── constants.ts # 模块常量
│ │ └── index.ts # 模块公开导出
│ └── order/
│
├── components/ # 全局共享 UI 组件
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.module.css
│ │ └── __tests__/
│ ├── Modal/
│ ├── Form/
│ └── ErrorBoundary/
│
├── hooks/ # 全局共享 hooks
│ ├── useAuth.ts
│ ├── useDebounce.ts
│ └── useMediaQuery.ts
│
├── services/ # API 基础层
│ ├── request.ts # Axios/fetch 实例与拦截器
│ └── endpoints/ # API 端点定义(如按领域拆分)
│
├── stores/ # 全局状态管理
│ ├── authStore.ts
│ └── uiStore.ts
│
├── locales/ # 国际化语言包
│ ├── zh-CN.json # 中文
│ ├── en-US.json # 英文
│ └── index.ts # i18n 实例初始化(i18next / react-intl)
│
├── assets/ # 静态资源
│ ├── images/ # 图片(PNG、JPG、WebP)
│ ├── icons/ # SVG 图标
│ └── fonts/ # 自定义字体
│
├── config/ # 应用配置
│ ├── env.ts # 环境变量类型化封装
│ └── features.ts # Feature Flags 管理
│
├── types/ # 全局共享类型
│ ├── api.ts # API 响应/请求通用类型
│ ├── models.ts # 业务实体类型
│ └── global.d.ts # 全局类型扩展(图片模块声明等)
│
├── utils/ # 纯工具函数
│ ├── format.ts # 日期、数字、货币格式化
│ ├── validators.ts # 表单校验规则
│ └── storage.ts # LocalStorage / SessionStorage 封装
│
├── styles/ # 全局样式与主题
│ ├── global.css # 全局基础样式(reset / normalize)
│ ├── variables.css # CSS 变量(颜色、间距、字号)
│ ├── breakpoints.ts # 响应式断点常量
│ └── themes/ # 主题定义
│ ├── light.css # 亮色主题变量
│ ├── dark.css # 暗色主题变量
│ └── index.ts # 主题切换逻辑
│
└── constants/ # 全局常量
├── routes.ts # 路由路径常量
└── config.ts # 业务常量(分页大小、超时时间等)
pages/ 做路由映射和布局组合,不放业务逻辑layouts/ 定义页面骨架(侧边栏、顶栏、面包屑),由路由配置引用features/ 按业务领域划分,模块内自包含(components + hooks + api + types)components/ 仅放无业务耦合的通用组件,可跨项目复用hooks/ 仅放通用逻辑(防抖、媒体查询等),业务 hooks 放到对应 feature 中locales/ 存放语言包 JSON 文件,组件中使用 t('key') 而非硬编码文案assets/ 存放静态资源,图标优先使用 SVG,图片优先使用 WebP/AVIFconfig/ 封装环境变量和 Feature Flags,禁止组件中直接读取 import.meta.envstyles/themes/ 通过 CSS 变量实现主题切换,组件中引用变量而非硬编码颜色index.ts 管控公开 API,避免深层路径导入ComponentName/
├── ComponentName.tsx
├── ComponentName.types.ts # 类型定义(如需独立)
├── ComponentName.module.css # 样式(如需)
├── hooks/
│ └── useComponentLogic.ts
├── components/
│ └── SubComponent.tsx
└── __tests__/
└── ComponentName.spec.tsx
页面组件 (Pages) → 路由映射、布局组合
└── 容器组件 (Containers) → 数据获取、状态编排
└── 业务组件 (Features) → 领域逻辑展示
└── 通用组件 (UI) → 纯展示,无业务耦合
ComponentNamePropshandle 前缀(如 handleClick)on 前缀(如 onChange、onSubmit)any,优先使用 unknown 或精确类型<T> 约束,保持类型推导interface TableProps<T extends Record<string, unknown>> {
data: T[];
columns: ColumnDef<T>[];
onRowClick?: (row: T) => void;
}
use 前缀命名function useUserList(params: QueryParams) {
const [data, setData] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const fetch = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await getUserList(params);
setData(res.list);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
}, [params]);
useEffect(() => { fetch(); }, [fetch]);
return { data, loading, error, refetch: fetch };
}
useEffect 必须有正确的依赖数组和清理函数useMemo / useCallback 仅在子组件使用 memo 或依赖稳定性重要时使用// app/routes.tsx
const routes: RouteObject[] = [
{
path: '/',
element: <MainLayout />,
children: [
{ index: true, element: <DashboardPage /> },
{ path: 'users', element: <UserListPage /> },
{ path: 'users/:id', element: <UserDetailPage /> },
{ path: 'settings', element: <SettingsPage /> },
],
},
{ path: '/login', element: <LoginPage /> },
{ path: '*', element: <NotFoundPage /> },
];
lazy + Suspense 按需加载const UserListPage = lazy(() => import('@/pages/UserList/UserListPage'));
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) return <Navigate to="/login" replace />;
return <>{children}</>;
}
| 状态类型 | 推荐方案 |
|---|---|
| 组件内临时 UI 状态 | useState / useReducer |
| 跨组件共享业务状态 | 全局 store(Zustand / Redux Toolkit) |
| 服务端数据缓存 | React Query / SWR |
| URL 驱动状态 | 路由参数 / useSearchParams |
| 表单状态 | React Hook Form / Formik |
// services/request.ts
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
});
request.interceptors.response.use(
(res) => res.data,
(error) => {
if (error.response?.status === 401) {
redirectToLogin();
}
return Promise.reject(normalizeError(error));
},
);
// features/user/api.ts
export function getUserList(params: UserQueryParams): Promise<PageResult<User>> {
return request.get('/users', { params });
}
export function updateUser(id: string, data: UpdateUserDTO): Promise<User> {
return request.put(`/users/${id}`, data);
}
每个功能模块应有 Error Boundary 包裹,避免局部错误导致全页白屏:
<ErrorBoundary fallback={<ModuleErrorFallback />}>
<UserDashboard />
</ErrorBoundary>
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function Dashboard() {
return (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={chartData} />
</Suspense>
);
}
lazy + Suspensefallback 使用 Skeleton 而非简单 spinnerdescribe('UserForm', () => {
it('should submit with valid data', async () => {
const onSubmit = vi.fn();
render(<UserForm onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText('用户名'), 'test');
await userEvent.click(screen.getByRole('button', { name: '提交' }));
expect(onSubmit).toHaveBeenCalledWith({ username: 'test' });
});
it('should show error on invalid input', async () => {
render(<UserForm onSubmit={vi.fn()} />);
await userEvent.click(screen.getByRole('button', { name: '提交' }));
expect(screen.getByText('用户名不能为空')).toBeInTheDocument();
});
});
screen.getByRole / getByLabelText 而非 getByTestIdReact.memo、useMemo、useCallbackkeyuseEffect + setState 模拟 computed(应直接在渲染中计算)components/ 中放业务耦合组件index.ts