mcpField
F
frontend-react-best-practices

frontend-react-best-practices

github.com/sergiodxa開発921.2k 回インストール38 週間前に更新
リポジトリ

frontend-react-best-practices について

frontend-react-best-practicesは、Reactコードの作成、レビュー、リファクタリングに使える性能最適化とコンポジションのガイドです。33のルールを6カテゴリに分け、再レンダリング、バンドルサイズ、Hooksなどを扱います。

主な機能

  1. 016カテゴリにわたる33のルール
  2. 02バンドルサイズとモジュール読み込みの最適化
  3. 03不要な再レンダリングの削減
  4. 04レンダリングとハイドレーションの改善
  5. 05Hooksの利用指針
  6. 06コンポーネント構成パターンの解説

活用シーン

  1. 01Reactコンポーネント作成時のルール確認
  2. 02コードレビューでの性能課題の特定
  3. 03Hooksやコンポーネント状態ロジックのリファクタリング
  4. 04アプリのバンドルサイズ最適化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/sergiodxa/agent-skills --skill frontend-react-best-practices

インストール後、Claude Code で/frontend-react-best-practicesを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

rules/
bundle-barrel-imports.md
bundle-conditional.md
bundle-preload.md
client-localstorage-schema.md
client-passive-event-listeners.md
composition-avoid-boolean-props.md
composition-avoid-overabstraction.md
composition-children-over-render-props.md
composition-compound-components.md
composition-explicit-variants.md
composition-state-provider.md
composition-typescript-namespaces.md
fault-tolerant-error-boundaries.md
hooks-limit-useeffect.md
hooks-useeffect-named-functions.md
rendering-animate-svg-wrapper.md
rendering-client-only.md
rendering-conditional-render.md
rendering-content-visibility.md
rendering-hoist-jsx.md
rendering-hydration-no-flicker.md
rendering-hydration-suppress-warning.md
rendering-svg-precision.md
rendering-use-hydrated.md
rendering-usetransition-loading.md
rerender-dependencies.md
rerender-derived-state-no-effect.md
rerender-derived-state.md
rerender-functional-setstate.md
rerender-lazy-state-init.md

詳細

React のベストプラクティス

React コンポーネントのパフォーマンス最適化とコンポジションパターン。再レンダリングの削減、バンドルの最適化、コンポーネントのコンポジション、よくある React の落とし穴の回避に重点を置いた、6 カテゴリにわたる 33 のルールを収録しています。

適用するタイミング

次のような場合に、これらのガイドラインを参照してください。

  • 新しい React コンポーネントを作成するとき
  • パフォーマンスの問題についてコードをレビューするとき
  • 既存の React コードをリファクタリングするとき
  • バンドルサイズを最適化するとき
  • フックと状態を扱うとき

ルールの概要

バンドルサイズの最適化(CRITICAL)
bundle-barrel-imports - @rules/bundle-barrel-imports.md

バレルファイルを避け、ソースから直接インポートします。

// Bad: loads entire library (200-800ms)
import { Check, X } from "lucide-react";

// Good: loads only what you need
import Check from "lucide-react/dist/esm/icons/check";
import X from "lucide-react/dist/esm/icons/x";
bundle-conditional - @rules/bundle-conditional.md

機能が有効になったときにのみモジュールを読み込みます。

useEffect(() => {
  if (enabled && typeof window !== "undefined") {
    import("./heavy-module").then((mod) => setModule(mod));
  }
}, [enabled]);
bundle-preload - @rules/bundle-preload.md

体感速度を高めるため、ホバー時やフォーカス時にプリロードします。

<button
  onMouseEnter={() => import("./editor")}
  onFocus={() => import("./editor")}
  onClick={openEditor}
>
  Open Editor
</button>
再レンダリングの最適化(MEDIUM)
rerender-functional-setstate - @rules/rerender-functional-setstate.md

安定したコールバックには関数形式の setState を使います。

// Bad: stale closure risk, recreates on items change
const addItem = useCallback(
  (item) => {
    setItems([...items, item]);
  },
  [items],
);

// Good: always uses latest state, stable reference
const addItem = useCallback((item) => {
  setItems((curr) => [...curr, item]);
}, []);
rerender-derived-state-no-effect - @rules/rerender-derived-state-no-effect.md

エフェクトではなく、レンダー中に状態を導出します。

// Bad: extra state and effect, extra render
const [fullName, setFullName] = useState("");
useEffect(() => {
  setFullName(firstName + " " + lastName);
}, [firstName, lastName]);

// Good: derived directly during render
const fullName = firstName + " " + lastName;
rerender-lazy-state-init - @rules/rerender-lazy-state-init.md

初期値の計算コストが高い場合は、関数を useState に渡します。

// Bad: runs expensiveComputation() on every render
const [data] = useState(expensiveComputation());

// Good: runs only on initial render
const [data] = useState(() => expensiveComputation());
rerender-dependencies - @rules/rerender-dependencies.md

エフェクトではプリミティブな依存値を使います。

// Bad: runs on any user field change
useEffect(() => {
  console.log(user.id);
}, [user]);

// Good: runs only when id changes
useEffect(() => {
  console.log(user.id);
}, [user.id]);
rerender-derived-state - @rules/rerender-derived-state.md

生の値ではなく、導出した真偽値を購読します。

// Bad: re-renders on every pixel change
const width = useWindowWidth();
const isMobile = width < 768;

// Good: re-renders only when boolean changes
const isMobile = useMediaQuery("(max-width: 767px)");
rerender-memo - @rules/rerender-memo.md

計算コストの高い処理を、メモ化したコンポーネントに切り出します。

// Good: skips computation when loading
const UserAvatar = memo(function UserAvatar({ user }) {
  let id = useMemo(() => computeAvatarId(user), [user]);
  return <Avatar id={id} />;
});

function Profile({ user, loading }) {
  if (loading) return <Skeleton />;
  return <UserAvatar user={user} />;
}
rerender-memo-with-default-value - @rules/rerender-memo-with-default-value.md

プリミティブでないデフォルトの props は、コンポーネントの外に定数として定義します。

// Bad: breaks memoization (new function each render)
const Button = memo(({ onClick = () => {} }) => ...)

// Good: stable default value
const NOOP = () => {}
const Button = memo(({ onClick = NOOP }) => ...)
rerender-simple-expression-in-memo - @rules/rerender-simple-expression-in-memo.md

単純なプリミティブ式を useMemo でラップしないでください。

// Bad: useMemo overhead > expression cost
const isLoading = useMemo(() => a.loading || b.loading, [a.loading, b.loading]);

// Good: just compute it
const isLoading = a.loading || b.loading;
rerender-move-effect-to-event - @rules/rerender-move-effect-to-event.md

操作のロジックはエフェクトではなく、イベントハンドラーに記述します。

// Bad: effect re-runs on theme change
useEffect(() => {
  if (submitted) post("/api/register");
}, [submitted, theme]);

// Good: in handler
const handleSubmit = () => post("/api/register");
rerender-transitions - @rules/rerender-transitions.md

緊急性の低い更新には startTransition を使います。

// Good: non-blocking scroll tracking
const handler = () => {
  startTransition(() => setScrollY(window.scrollY));
};
rerender-use-ref-transient-values - @rules/rerender-use-ref-transient-values.md

頻繁に変わる一時的な値には ref を使います。

// Good: no re-render, direct DOM update
const lastXRef = useRef(0);
const dotRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  let onMove = (e) => {
    lastXRef.current = e.clientX;
    dotRef.current?.style.transform = `translateX(${e.clientX}px)`;
  };
  window.addEventListener("mousemove", onMove);
  return () => window.removeEventListener("mousemove", onMove);
}, []);
レンダリングのパフォーマンス(MEDIUM)
rendering-conditional-render - @rules/rendering-conditional-render.md

数値を含む条件分岐では、&& ではなく三項演算子を使います。

// Bad: renders "0" when count is 0
{
  count && <Badge>{count}</Badge>;
}

// Good: renders nothing when count is 0
{
  count > 0 ? <Badge>{count}</Badge> : null;
}
rendering-hoist-jsx - @rules/rendering-hoist-jsx.md

静的な JSX はコンポーネントの外に切り出します。

// Good: reuses same element, especially for large SVGs
const skeleton = <div className="animate-pulse h-20 bg-gray-200" />;

function Container({ loading }) {
  return loading ? skeleton : <Content />;
}
rendering-content-visibility - @rules/rendering-content-visibility.md

長いリストには content-visibility を使います。

.list-item {
  content-visibility: auto;
  contain-intrinsic-size: 0 80px;
}
rendering-animate-svg-wrapper - @rules/rendering-animate-svg-wrapper.md

GPU アクセラレーションを利用するため、SVG 要素ではなくラッパーの div をアニメーションさせます。

// Good: hardware accelerated
<div className="animate-spin">
  <svg>...</svg>
</div>
rendering-svg-precision - @rules/rendering-svg-precision.md

SVGO で SVG 座標の精度を下げます。

npx svgo --precision=1 --multipass icon.svg
rendering-hydration-no-flicker - @rules/rendering-hydration-no-flicker.md

ちらつきを防ぐため、クライアント専用データにはインラインスクリプトを使います。

<div id="theme-wrapper">{children}</div>
<script dangerouslySetInnerHTML={{ __html: `
  var theme = localStorage.getItem('theme') || 'light';
  document.getElementById('theme-wrapper').className = theme;
` }} />
rendering-hydration-suppress-warning - @rules/rendering-hydration-suppress-warning.md

想定されるハイドレーションの不一致は警告を抑制します。

<span suppressHydrationWarning>{new Date().toLocaleString()}</span>
rendering-client-only - @rules/rendering-client-only.md

ブラウザー専用のコンポーネントは、ClientOnly とフォールバックを使ってレンダリングします。

<ClientOnly fallback={<Skeleton />}>
  {() => <Map />}
</ClientOnly>
rendering-use-hydrated - @rules/rendering-use-hydrated.md

SSR と CSR の差異には useHydrated を使います。

let hydrated = useHydrated();
return hydrated ? <Widget /> : <Skeleton />;
rendering-usetransition-loading - @rules/rendering-usetransition-loading.md

手動の読み込み状態よりも useTransition を優先します。

const [isPending, startTransition] = useTransition();

let handleSearch = (value) => {
  startTransition(async () => {
    let data = await fetchResults(value);
    setResults(data);
  });
};
fault-tolerant-error-boundaries - @rules/fault-tolerant-error-boundaries.md

機能の境界にエラーバウンダリーを配置します。

<ErrorBoundary fallback={<SidebarError />}>
  <Sidebar />
</ErrorBoundary>
クライアントのパターン(MEDIUM)
client-passive-event-listeners - @rules/client-passive-event-listeners.md

スクロールやタッチにはパッシブリスナーを使用します。

document.addEventListener("wheel", handler, { passive: true });
document.addEventListener("touchstart", handler, { passive: true });
client-localstorage-schema - @rules/client-localstorage-schema.md

localStorage のデータはバージョン管理し、最小限に抑えます。

const VERSION = "v2";

function saveConfig(config: Config) {
  try {
    localStorage.setItem(`config:${VERSION}`, JSON.stringify(config));
  } catch {} // Handle incognito/quota exceeded
}
フック(HIGH)
hooks-limit-useeffect - @rules/hooks-limit-useeffect.md

useEffect は本当に必要な場合にのみ使用します。派生状態またはイベントハンドラーを優先します。

// Bad: useEffect to derive state
let [filtered, setFiltered] = useState(items);
useEffect(() => {
  setFiltered(items.filter((i) => i.active));
}, [items]);

// Good: derive during render
let filtered = items.filter((i) => i.active);

// Good: useMemo if expensive
let filtered = useMemo(() => items.filter((i) => i.active), [items]);
hooks-useeffect-named-functions - @rules/hooks-useeffect-named-functions.md

デバッグしやすく、自己文書化にも役立つように、useEffect 内では名前付き関数宣言を使用します。

// Bad: anonymous arrow function
useEffect(() => {
  document.title = title;
}, [title]);

// Good: named function
useEffect(
  function syncDocumentTitle() {
    document.title = title;
  },
  [title],
);

// Good: also name cleanup functions
useEffect(function subscribeToOnlineStatus() {
  window.addEventListener("online", handleOnline);
  return function unsubscribeFromOnlineStatus() {
    window.removeEventListener("online", handleOnline);
  };
}, []);
コンポジションパターン(HIGH)
composition-avoid-boolean-props - @rules/composition-avoid-boolean-props.md

動作をカスタマイズするためにブール型の props を追加しないでください。代わりにコンポジションを使用します。

// Bad: boolean prop explosion
<Composer isThread isEditing={false} showAttachments />

// Good: explicit variants
<ThreadComposer channelId="abc" />
<EditComposer messageId="xyz" />
composition-compound-components - @rules/composition-compound-components.md

複雑なコンポーネントは、共有コンテキストを持つ複合コンポーネントとして構成します。

// Good: compound components
<Composer.Provider state={state} actions={actions}>
  <Composer.Frame>
    <Composer.Input />
    <Composer.Footer>
      <Composer.Submit />
    </Composer.Footer>
  </Composer.Frame>
</Composer.Provider>
composition-state-provider - @rules/composition-state-provider.md

コンポーネント間で状態にアクセスできるように、プロバイダーコンポーネントに状態を持ち上げます。

// Good: state in provider, accessible anywhere inside
<ForwardMessageProvider>
  <Dialog>
    <Composer.Input />
    <MessagePreview /> {/* Can read state */}
    <ForwardButton /> {/* Can call submit */}
  </Dialog>
</ForwardMessageProvider>
composition-explicit-variants - @rules/composition-explicit-variants.md

props の組み合わせではなく、明示的なバリアントコンポーネントを作成します。

// Good: self-documenting variants
function ThreadComposer({ channelId }) {
  return (
    <ThreadProvider channelId={channelId}>
      <Composer.Frame>
        <Composer.Input />
        <AlsoSendToChannelField />
        <Composer.Submit />
      </Composer.Frame>
    </ThreadProvider>
  );
}
composition-children-over-render-props - @rules/composition-children-over-render-props.md

コンポジションには children を優先します。データを受け渡す場合にのみ render props を使用します。

// Good: children for structure
<Card>
  <Card.Header>Title</Card.Header>
  <Card.Body>Content</Card.Body>
</Card>

// OK: render props when passing data
<List renderItem={({ item }) => <Item {...item} />} />
composition-avoid-overabstraction - @rules/composition-avoid-overabstraction.md

柔軟性の低い設定用 props は避け、組み合わせ可能な children API を優先します。

<Select value="abc" onChange={...}>
  <Option value="abc">ABC</Option>
  <Option value="xyz">XYZ</Option>
</Select>
composition-typescript-namespaces - @rules/composition-typescript-namespaces.md

コンポーネントとその型を TypeScript の名前空間でまとめ、単一の import でアクセスできるようにします。

// components/button.tsx
export namespace Button {
  export type Variant = "solid" | "ghost" | "outline";
  export interface Props {
    variant?: Variant;
    children: React.ReactNode;
  }
}

export function Button({ variant = "solid", children }: Button.Props) {
  // ...
}

// Usage: single import
import { Button } from "~/components/button";

<Button variant="ghost">Click</Button>
function wrap(props: Button.Props) { ... }

重要: 名前空間には型のみを含め、実行時コードは含めないでください。

コメント