React 规范
本文档旨在为 React 项目提供一套统一的编码规范和最佳实践,以提高代码质量、可读性、可维护性,并保障应用的性能和安全。规范结合了 Airbnb React/JSX Style Guide,并根据项目集成的 ESLint 规则(包括 JS/TS 基础规则和 React 特定规则)进行了定制。
1. 基本约定
1.1 文件扩展名
React 组件文件应使用 .tsx 扩展名。
MyComponent.tsx;
MyComponent.js;
MyComponent.ts;
1.2 组件定义
- 优先使用函数声明或函数表达式来定义具名组件。
- 匿名组件(例如,作为参数传递时)可以使用箭头函数表达式。
interface MyComponentProps {
name: string;
}
// 一个简单的组件示例
function MyComponent(props: MyComponentProps) {
const { name } = props;
return <div>{name}</div>;
}
const MyComponentAsExpression = function(props: MyComponentProps) {
const { name } = props;
return <div>{name}</div>;
};
// 默认导出的组件
export default function DefaultComponent() {
// ...
return <div>Default</div>;
}
const MyArrowComponent = (props: MyComponentProps) => (
<div>{props.name}</div>
);
1.3 避免在 JSX 中引入 React
从 React 17 开始,新的 JSX 转换不再需要 import React from 'react'。项目中已禁用此规则,请勿在文件中引入未使用的 React。
function Greeting({ name }: { name: string }) {
return <div>Hello, {name}</div>;
}
import React from 'react';
function GreetingWithUnusedReact({ name }: { name: string }) {
return <div>Hello, {name}</div>;
}
2. 代码风格
2.1 JSX 语法
2.1.1 引号
JSX 属性值优先使用双引号 (")。
普通 JS/TS 字符串优先使用单引号 (')。
对于不需要转义的字符串常量,可以直接使用,不需要花括号。
<Greeting name="John" message={'Hello World'} />;
<Greeting name='John' />;
<Greeting name={"John"} />;
2.1.2 布尔属性
如果属性值为 true,请省略该值。
<Checkbox checked />;
<Checkbox checked={true} />;
2.1.3 标签闭合
- 没有子元素的组件应使用自闭合标签。
- 自闭合标签的斜杠前应有一个空格。
<MyComponent />;
<MyComponent></MyComponent>;
<MyComponent/>;
2.1.4 括号包裹多行 JSX
当 JSX 结构跨越多行时,必须用括号 () 包裹起来。
function MyComponent() {
return (
<div>
<h1>Title</h1>
<p>Paragraph</p>
</div>
);
}
function MyComponent() {
return <div>
<h1>Title</h1>
<p>Paragraph</p>
</div>;
}
2.1.5 属性换行和缩进
- 当组件有多个属性时,建议每个属性占一行。
- 第一个属性不应换行。
- 属性使用两个空格进行缩进。
<MyComponent
propA="valueA"
propB="valueB"
propC="valueC"
/>;
<MyComponent propA="valueA" propB="valueB" />;
<MyComponent
propA="valueA"
propB="valueB"
/>;
2.2 花括号间距
JSX 的花括号内侧不应有空格。
<MyComponent name={userName} />;
<MyComponent name={ userName } />;
3. 组件与 Props
3.1 Props 解构
在函数组件的参数中直接解构 props。
interface MyComponentProps {
name: string;
age: number;
}
function MyComponent({ name, age }: MyComponentProps) {
return <div>{`${name} is ${age} years old.`}</div>;
}
interface MyComponentProps {
name: string;
age: number;
}
function MyComponent(props: MyComponentProps) {
return <div>{`${props.name} is ${props.age} years old.`}</div>;
}
3.2 默认值
对于非必需的 props,优先使用 TypeScript 的可选链和默认参数来设置默认值。
interface GreetingProps {
name?: string;
}
function Greeting({ name = 'Guest' }: GreetingProps) {
return <div>Hello, {name}</div>;
}
function Greeting({ name }: GreetingProps) {
const finalName = name || 'Guest';
return <div>Hello, {finalName}</div>;
}
3.3 禁止 Props 扩散
避免使用 ... 扩展操作符传递 props,除非是明确为了透传 props 到子组件(例如在高阶组件或样式化组件中)。明确地列出每个 prop 可以让代码更清晰,也更容易进行类型检查和重构。
interface UserProfileProps {
name: string;
avatar: string;
}
/**
* 仅显示用户头像。
* @param props 包含头像 URL。
*/
function UserAvatar({ avatar }: { avatar: string }) {
return <img src={avatar} alt="User Avatar" />;
}
/**
* 显示完整的用户资料。
* @param props 包含用户名和头像。
*/
function UserProfile({ name, avatar }: UserProfileProps) {
return (
<div>
<span>{name}</span>
<UserAvatar avatar={avatar} />
</div>
);
}
function UserProfileWithSpread(props: UserProfileProps) {
return (
<div>
<span>{props.name}</span>
{/* 不应将所有 props 都传递下去 */}
<UserAvatar {...props} />
</div>
);
}
3.4 避免使用数组索引作为 key
在渲染列表时,应使用稳定且唯一的标识符作为 key,避免使用数组的索引。
items.map((item) => <ListItem key={item.id} item={item} />);
items.map((item, index) => <ListItem key={index} item={item} />);
3.5 组件命名
组件名称使用帕斯卡命名法 (PascalCase)。
function UserProfile() { /* ... */ }
function userProfile() { /* ... */ }
4. State、Hooks 与 Compiler
4.1 核心 Hooks 使用规则
- 只在顶层调用 Hooks:不要在循环、条件或嵌套函数中调用 Hooks。
- 只在 React 函数中调用 Hooks:只能在函数组件或自定义 Hooks 中调用 Hooks。
import { useState, useEffect } from 'react';
function MyComponent({ condition }: { condition: boolean }) {
const [name, setName] = useState('John'); // 在顶层调用
useEffect(() => {
// ...
}, [condition]); // 在顶层调用
if (!condition) {
return null;
}
// ...
}
import { useState, useEffect } from 'react';
function MyComponentWithBadHooks({ condition }: { condition: boolean }) {
if (condition) {
const [name, setName] = useState('John'); // 错误:在条件语句中调用
}
useEffect(() => {
// ...
});
}
4.2 useEffect 依赖项
useEffect、useCallback、useMemo 等 Hooks 必须包含所有外部依赖项。
userIdinterface User {
id: string;
name: string;
}
declare function fetchUser(userId: string): Promise<User>;
function UserInfo({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchUser(userId).then(setUser).catch(console.error);
}, [userId]);
return <div>{user?.name}</div>;
}
userIdinterface User {
id: string;
name: string;
}
declare function fetchUser(userId: string): Promise<User>;
function UserInfo({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchUser(userId).then(setUser).catch(console.error);
}, []); // 这会导致 userId 变化时,数据不会重新获取
return <div>{user?.name}</div>;
}
4.3 useState 命名
使用数组解构,并为 state 变量和其设置函数采用对称命名(例如 [name, setName])。
const [count, setCount] = useState(0);
const [countValue, updateCount] = useState(0);
4.4 为 React Compiler 编写代码
为了使代码与未来的 React Compiler (Forget) 兼容,我们需要遵循更严格的规则,确保组件和 Hooks 的“纯净性”。
4.4.1 保持组件和 Hooks 纯净
组件和 Hooks 应该像纯函数一样,对于相同的输入(props, state),总是返回相同的输出(UI),并且没有可观察的副作用。
禁止在渲染期间修改 State:不要在组件的顶层作用域或 render 逻辑中调用 setState。
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return <button type="button" onClick={handleClick}>{count}</button>;
}
function BrokenCounter() {
const [count, setCount] = useState(0);
// 错误:在 render 逻辑中直接调用 setState
setCount(count + 1);
return <div>{count}</div>;
}
4.4.2 状态和属性的不可变性
不要直接修改 Props 或 State:Props 和 State 都应被视为不可变的。要更新它们,请使用 setState 并创建新的对象或数组。
function Profile() {
const [user, setUser] = useState({ name: 'John', age: 30 });
const handleBirthday = () => {
setUser(currentUser => ({ ...currentUser, age: currentUser.age + 1 }));
};
return <div onClick={handleBirthday}>{user.name}: {user.age}</div>
}
function BrokenProfile() {
const [user, setUser] = useState({ name: 'John', age: 30 });
const handleBirthday = () => {
// 错误:直接修改了 state 对象
user.age += 1;
setUser(user); // 这可能不会触发重新渲染
};
return <div onClick={handleBirthday}>{user.name}: {user.age}</div>
}
5. 性能优化
5.1 避免在 Props 中创建新对象、数组或函数
在渲染过程中,每次都创建新的对象、数组或函数实例会导致子组件不必要的重新渲染。
5.1.1 对象
useMemoconst containerStyle = { padding: '10px' };
function MyComponent() {
return <div style={containerStyle}>Content</div>;
}
useMemoimport { useMemo } from 'react';
function MyComponentWithMemo({ theme }: { theme: { color: string } }) {
const memoizedStyle = useMemo(() => ({
backgroundColor: theme.color,
}), [theme.color]);
return <div style={memoizedStyle}>Content</div>;
}
function MyComponentWithNewObject() {
return <div style={{ padding: '10px' }}>Content</div>;
}
5.1.2 数组
const defaultOptions = ['Option 1', 'Option 2'];
function MySelectComponent() {
return <Select options={defaultOptions} />;
}
function MySelectComponentWithNewArray() {
return <Select options={['Option 1', 'Option 2']} />;
}
5.1.3 函数
使用 useCallback 来记忆化函数,或者将函数定义在组件外部。
useCallbackimport { useCallback } from 'react';
function MyComponent() {
const handleClick = useCallback(() => {
console.log('Clicked!');
}, []);
return <MyButton onClick={handleClick} />;
}
function MyComponentWithNewFunction() {
return <MyButton onClick={() => console.log('Clicked!')} />;
}
5.2 避免定义不稳定的嵌套组件
不要在另一个组件的渲染函数内部定义组件。这会导致嵌套组件在每次父组件渲染时都被重新创建,从而丢失其所有状态。
function ListItem({ item }: { item: { id: string; name: string } }) {
return <li>{item.name}</li>;
}
function MyList({ items }: { items: { id: string; name: string }[] }) {
return (
<ul>
{items.map((item) => <ListItem key={item.id} item={item} />)}
</ul>
);
}
function MyListWithNestedComponent({ items }: { items: { id: string; name: string }[] }) {
// ListItem 在每次 MyList 渲染时都会被重新创建
function NestedListItem({ item }: { item: { name: string } }) {
return <li>{item.name}</li>;
}
return (
<ul>
{items.map((item) => <NestedListItem key={item.id} item={item} />)}
</ul>
);
}
6. 可访问性 (a11y)
6.1 图像 alt 属性
所有 <img> 标签必须有一个 alt 属性。对于装饰性图片,可以设置为空字符串 alt=""。
<img src="avatar.png" alt="User's avatar" />;
<img src="divider.png" alt="" />;
<img src="avatar.png" />;
6.2 锚点 <a> 标签
<a>标签必须有内容。<a>标签必须具有有效的href属性,或者用<button>代替。
<a href="/about">About Us</a>;
<button type="button" onClick={handleClick}>Click Me</button>;
<a href="#">Click Me</a>;
<a onClick={handleClick}>Click Me</a>;
6.3 ARIA 属性
- 使用有效的 ARIA 属性。
- 不要使用不支持的 ARIA 属性。
<div role="button" aria-pressed="false">Press me</div>;
<div role="datepicker">Invalid Role</div>;
<div aria-lorem="ipsum">Invalid ARIA attribute</div>;
6.4 交互元素
具有点击事件的可见非交互元素(如 <div>、<span>)应具有 role 属性,并处理键盘事件。通常,最好直接使用 <button> 或 <a>。
<button type="button" onClick={handleClick}>Action</button>;
<div onClick={handleClick}>Action</div>;
7. 数据请求 (TanStack Query)
7.1 Hooks 依赖
useQuery 和 useMutation 的 queryKey 和其他依赖项必须是稳定的。避免在 queryKey 中使用非序列化的值,如函数或非稳定对象。
queryKey 是稳定的function TodoList({ listId }: { listId: string }) {
const { data } = useQuery({
queryKey: ['todos', listId],
queryFn: () => fetchTodos(listId),
});
// ...
}
queryKey 包含了不稳定的对象引用function TodoListWithUnstableKey({ filter }: { filter: object }) {
// filter 对象每次渲染都可能是新的引用
const { data } = useQuery({
queryKey: ['todos', filter],
queryFn: () => fetchTodos(filter),
});
// ...
}
function TodoListWithStableKey({ filter }: { filter: object }) {
const queryKey = useMemo(() => ['todos', filter], [filter]);
const { data } = useQuery({
queryKey,
queryFn: () => fetchTodos(filter),
});
// ...
}
7.2 queryClient 稳定性
QueryClient 实例应该是稳定的,通常在应用顶层创建一次,并通过 React Context 提供。
clientconst queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
clientfunction YourAppWithUnstableClient() {
// 每次渲染都会创建新的 client 实例,导致缓存丢失
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
{/* ... */}
</QueryClientProvider>
);
}
7.3 查询函数 queryFn
查询函数 queryFn 必须返回一个 Promise。不要使用返回 void 的函数。
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos, // fetchTodos 返回一个 Promise
});
useQuery({
queryKey: ['todos'],
queryFn: () => {
// 只是调用函数,没有返回 Promise
fetchTodos();
},
});
8. 安全性
8.1 警惕 XSS 攻击
避免使用可能导致跨站脚本(XSS)攻击的属性和函数,如 dangerouslySetInnerHTML。如果必须使用,请确保内容是经过严格清理和消毒的。
function SafeComponent({ content }: { content: string }) {
return <div>{content}</div>;
}
function UnsafeComponent({ rawHTML }: { rawHTML: string }) {
return <div dangerouslySetInnerHTML={{ __html: rawHTML }} />;
}
8.2 javascript: URL
禁止在 href 等属性中使用 javascript: 协议的 URL。
<a href="/some/path">Navigate</a>;
<button type="button" onClick={doSomething}>Do Something</button>;
<a href="javascript:doSomething()">Do Something</a>;
8.3 target="_blank"
当使用 target="_blank" 打开新标签页时,必须同时添加 rel="noopener noreferrer" 以防止安全漏洞。
<a href={url} target="_blank" rel="noopener noreferrer">
Open in new tab
</a>;
<a href={url} target="_blank">
Open in new tab
</a>;