--- title: 样式指南 icon: 画笔 description: 为 Twenty 做出贡献的代码约定和最佳实践。 --- ## React ### 仅使用函数组件 始终使用带命名导出的 TSX 函数组件。 ```tsx // ❌ Bad const MyComponent = () => { return
Hello World
; }; export default MyComponent; // ✅ Good export function MyComponent() { return
Hello World
; }; ``` ### 属性 创建一个名为 `{ComponentName}Props` 的类型。 使用解构。 不要使用 `React.FC`。 ```tsx type MyComponentProps = { name: string; }; export const MyComponent = ({ name }: MyComponentProps) =>
Hello {name}
; ``` ### 禁止仅为单个 prop 使用展开 ```tsx // ❌ Bad const MyComponent = (props: MyComponentProps) => ; // ✅ Good const MyComponent = ({ prop1, prop2 }: MyComponentProps) => ; ``` ## 状态管理 ### 使用 Jotai 原子管理全局状态 ```tsx import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; export const myAtomState = createAtomState({ key: 'myAtomState', defaultValue: 'default value', }); ``` * 优先使用原子,而非 props 逐层传递 * 不要将 `useRef` 用于状态 — 使用 `useState` 或原子。 * 对列表使用原子族和选择器。 ### 避免不必要的重新渲染 * 将 `useEffect` 和数据获取提取到同级的 sidecar 组件中。 * 优先使用事件处理器(`handleClick`、`handleChange`)而不是 `useEffect`。 * 不要使用 `React.memo()` — 请改为修复根本原因。 * 限制 `useCallback` / `useMemo` 的使用。 ```tsx // ❌ Bad — useEffect in the same component causes re-renders export const Page = () => { const [data, setData] = useAtomState(dataState); const [dep] = useAtomState(depState); useEffect(() => { setData(dep); }, [dep]); return
{data}
; }; // ✅ Good — extract into sibling export const PageData = () => { const [data, setData] = useAtomState(dataState); const [dep] = useAtomState(depState); useEffect(() => { setData(dep); }, [dep]); return <>; }; export const Page = () => { const [data] = useAtomState(dataState); return
{data}
; }; ``` ## TypeScript * **优先用 `type` 而非 `interface`** — 更灵活、更易组合。 * **优先用字符串字面量而非枚举** — 但 GraphQL 代码生成的枚举和内部库 API 除外。 * **禁止 `any`** — 强制启用严格的 TypeScript。 * **不使用类型导入** — 使用常规导入(由 Oxlint `typescript/consistent-type-imports` 强制执行)。 * **使用 [Zod](https://github.com/colinhacks/zod)** 对无类型对象进行运行时校验。 ## JavaScript ```tsx // Use nullish-coalescing (??) instead of || const value = process.env.MY_VALUE ?? 'default'; // Use optional chaining onClick?.(); ``` ## 命名 * **变量**:使用 camelCase,具描述性(`email` 而非 `value`,`fieldMetadata` 而非 `fm`)。 * **常量**:SCREAMING_SNAKE_CASE * **类型/类**:PascalCase * **文件/目录**:kebab-case(`.component.tsx`、`.service.ts`、`.entity.ts`) * **事件处理器**:`handleClick`(处理函数不要使用 `onClick` 作为名称) * **组件 props**:以组件名作为前缀(`ButtonProps`)。 * **样式化组件**:以 `Styled` 作为前缀(`StyledTitle`)。 ## 样式 使用 [Linaria](https://github.com/callstack/linaria) 的样式化组件。 使用主题值 — 避免硬编码 `px`、`rem` 或颜色。 ```tsx // ❌ Bad const StyledButton = styled.button` color: #333333; font-size: 1rem; margin-left: 4px; `; // ✅ Good const StyledButton = styled.button` color: ${({ theme }) => theme.font.color.primary}; font-size: ${({ theme }) => theme.font.size.md}; margin-left: ${({ theme }) => theme.spacing(1)}; `; ``` ## 导入 使用别名而不是相对路径: ```tsx // ❌ Bad import { Foo } from '../../../../../testing/decorators/Foo'; // ✅ Good import { Foo } from '~/testing/decorators/Foo'; import { Bar } from '@/modules/bar/components/Bar'; ``` ## 文件夹架构 ``` front └── modules/ # Feature modules │ └── module1/ │ ├── components/ │ ├── constants/ │ ├── contexts/ │ ├── graphql/ (fragments, queries, mutations) │ ├── hooks/ │ ├── states/ (atoms, selectors) │ ├── types/ │ └── utils/ └── pages/ # Route-level components └── ui/ # Reusable UI components (display, input, feedback, ...) ``` * 模块可以相互导入,但 `ui/` 应保持无依赖 * 将模块私有代码放在 `internal/` 子文件夹中 * 组件不超过 300 行,服务不超过 500 行。