a0493dbc4b
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com>
177 lines
5.4 KiB
Plaintext
177 lines
5.4 KiB
Plaintext
---
|
||
title: スタイルガイド
|
||
icon: paintbrush
|
||
description: Twentyに貢献するためのコード規約とベストプラクティス。
|
||
---
|
||
|
||
## React
|
||
|
||
### 関数コンポーネントのみ
|
||
|
||
常に名前付きエクスポートの TSX 関数コンポーネントを使用してください。
|
||
|
||
```tsx
|
||
// ❌ Bad
|
||
const MyComponent = () => {
|
||
return <div>Hello World</div>;
|
||
};
|
||
export default MyComponent;
|
||
|
||
// ✅ Good
|
||
export function MyComponent() {
|
||
return <div>Hello World</div>;
|
||
};
|
||
```
|
||
|
||
### プロパティ
|
||
|
||
`{ComponentName}Props` という名前の型を作成する。 分割代入を使用する。 `React.FC` を使用しない。
|
||
|
||
```tsx
|
||
type MyComponentProps = {
|
||
name: string;
|
||
};
|
||
|
||
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
|
||
```
|
||
|
||
### 単一変数によるプロップスのスプレッドは禁止
|
||
|
||
```tsx
|
||
// ❌ Bad
|
||
const MyComponent = (props: MyComponentProps) => <Other {...props} />;
|
||
|
||
// ✅ Good
|
||
const MyComponent = ({ prop1, prop2 }: MyComponentProps) => <Other {...{ prop1, prop2 }} />;
|
||
```
|
||
|
||
## 状態管理
|
||
|
||
### グローバル状態には Jotai の atom を使用
|
||
|
||
```tsx
|
||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||
|
||
export const myAtomState = createAtomState<string>({
|
||
key: 'myAtomState',
|
||
defaultValue: 'default value',
|
||
});
|
||
```
|
||
|
||
* prop drilling よりも atom を優先する
|
||
* 状態管理に `useRef` を使用しない — `useState` または atom を使用する
|
||
* リストには atom ファミリーとセレクターを使用する
|
||
|
||
### 不要な再レンダーを避ける
|
||
|
||
* `useEffect` とデータ取得は同一階層のサイドカーコンポーネントに切り出す
|
||
* `useEffect` よりもイベントハンドラー(`handleClick`、`handleChange`)を優先する
|
||
* `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 <div>{data}</div>;
|
||
};
|
||
|
||
// ✅ 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 <div>{data}</div>;
|
||
};
|
||
```
|
||
|
||
## TypeScript
|
||
|
||
* **`interface` より `type`** — より柔軟で、合成しやすい
|
||
* **enum より文字列リテラル** — ただし GraphQL コード生成の enum と内部ライブラリ 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、意味がわかる名前(`value` ではなく `email`、`fm` ではなく `fieldMetadata`)
|
||
* **定数**: SCREAMING_SNAKE_CASE
|
||
* **型/クラス**: PascalCase
|
||
* **ファイル/ディレクトリ**: kebab-case (`.component.tsx`, `.service.ts`, `.entity.ts`)
|
||
* **イベントハンドラー**: `handleClick` (ハンドラー関数として `onClick` は使用しない)
|
||
* **コンポーネントのプロップス**: コンポーネント名を接頭辞に付ける(`ButtonProps`)
|
||
* **スタイル付きコンポーネント**: `Styled` を接頭辞に付ける(`StyledTitle`)
|
||
|
||
## スタイリング
|
||
|
||
[Linaria](https://github.com/callstack/linaria) の styled components を使用する。 テーマの値を使用し、`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 行未満
|