# Feature-Sliced Design ## documentation - [実装例](/examples.md): FSDを使って作られたプロジェクト一覧 - [🧭 ナビゲーション](/nav.md): Feature-Sliced Design Navigation help page - [ドキュメントを検索](/search.md) - [Feature-Sliced Designのバージョン](/versions.md): Feature-Sliced Design Versions page listing all documented site versions - [💫 Community](/community.md): Community resources, additional materials - [Team](/community/team.md): Core-team - [代替案](/docs/about/alternatives.md): ビッグボールオブマッド - [ミッション](/docs/about/mission.md): ここでは、私たちがFSD方法論を開発する際に従う方法論適用の制限と目標について説明します。 - [モチベーション](/docs/about/motivation.md): Feature-Sliced Designの主なアイデアは、さまざまな開発者の経験を議論し、研究結果を統合することに基づいて、複雑で発展するプロジェクトの開発を容易にし、開発コストを削減することです。 - [会社での推進](/docs/about/promote/for-company.md) - [チームでの推進](/docs/about/promote/for-team.md) - [統合の側面](/docs/about/promote/integration.md): 利点: - [部分的な適用](/docs/about/promote/partial-application.md) - [抽象化](/docs/about/understanding/abstractions.md): 漏れのある抽象化の法則 - [アーキテクチャ](/docs/about/understanding/architecture.md): 問題 - [プロジェクトにおける知識の種類](/docs/about/understanding/knowledge-types.md): どのプロジェクトにも以下の「知識の種類」が存在します。 - [ネーミング](/docs/about/understanding/naming.md): 異なる開発者は異なる経験と背景を持っているため、同じエンティティが異なる名前で呼ばれることによって、チーム内で誤解が生じる可能性があります。例えば - [ニーズの理解と課題の定義について](/docs/about/understanding/needs-driven.md): — 新しい機能が解決する目標を明確にできませんか?それとも、問題はタスク自体が明確にされていないことにありますか?**FSDは、問題の定義や目標を引き出す手助けをすることも目的にしています。** - [アーキテクチャのシグナル](/docs/about/understanding/signals.md) - [ブランドガイドライン](/docs/branding.md): FSDのビジュアルアイデンティティは、そのコアコンセプトである Layered、Sliced self-contained parts、Parts & Compose、Segmented に基づいています。しかし、私たちはFSDの哲学を反映し、簡単に認識できる美しいアイデンティティを目指しています。 - [分解のチートシート](/docs/get-started/cheatsheet.md): インターフェースをレイヤーに分割する際の参考書として使用してください。以下にPDFバージョンもあり、印刷して枕の下に置いておくことができます。 - [FAQ](/docs/get-started/faq.md): 質問は、Discordコミュニティ、GitHub Discussions、およびTelegramチャットで聞くことができます。 - [概要](/docs/get-started/overview.md): Feature-Sliced Design (FSD) とは、フロントエンドアプリケーションの設計方法論です。簡単に言えば、コードを整理するためのルールと規約の集大成です。FSDの主な目的は、ビジネス要件が絶えず変化する中で、プロジェクトをより理解しやすく、構造化されたものにすることです。 - [チュートリアル](/docs/get-started/tutorial.md): 第1章 紙の上で - [Handling API Requests](/docs/guides/examples/api-requests.md): handling-api-requests} - [認証](/docs/guides/examples/auth.md): 一般的に、認証は以下のステップで構成されます。 - [自動補完](/docs/guides/examples/autocompleted.md) - [ブラウザAPI](/docs/guides/examples/browser-api.md) - [CMS](/docs/guides/examples/cms.md) - [フィードバック](/docs/guides/examples/feedback.md) - [i18n](/docs/guides/examples/i18n.md) - [メトリクス](/docs/guides/examples/metric.md) - [モノレポ](/docs/guides/examples/monorepo.md) - [ページレイアウト](/docs/guides/examples/page-layout.md): このガイドでは、複数のページが同じ構造を持ち、主な内容だけが異なる場合のページレイアウトの抽象化について説明します。 - [デスクトップ/タッチプラットフォーム](/docs/guides/examples/platforms.md) - [SSR](/docs/guides/examples/ssr.md) - [テーマ化](/docs/guides/examples/theme.md) - [型](/docs/guides/examples/types.md): このガイドでは、TypeScriptのような型付き言語のデータの型と、それがFSDにどのように適合するかについて説明します。 - [ホワイトラベル](/docs/guides/examples/white-labels.md) - [クロスインポート](/docs/guides/issues/cross-imports.md): クロスインポートは、レイヤーや抽象化が本来の責任以上に多くの責任を持ち始めると発生する。そのため、FSDは新しいレイヤーを設けて、これらのクロスインポートを分離することを可能にしている。 - [デセグメンテーション](/docs/guides/issues/desegmented.md): 状況 - [ルーティング](/docs/guides/issues/routes.md): 状況 - [カスタムアーキテクチャからの移行](/docs/guides/migration/from-custom.md): このガイドは、カスタムのアーキテクチャからFeature-Sliced Designへの移行に役立つアプローチを説明します。 - [v1からv2への移行](/docs/guides/migration/from-v1.md): なぜv2なのか? - [v2.0からv2.1への移行](/docs/guides/migration/from-v2-0.md): v2.1の主な変更点は、インターフェースを分解するための「ページファースト」という新しいメンタルモデルです。 - [Usage with Electron](/docs/guides/tech/with-electron.md): Electron applications have a special architecture consisting of multiple processes with different responsibilities. Applying FSD in such a context requires adapting the structure to the Electron specifics. - [NextJSとの併用](/docs/guides/tech/with-nextjs.md): NextJSプロジェクトでFSDを実装することは可能ですが、プロジェクトの構造に関するNextJSの要件とFSDの原則の間に2つの点で対立が生じます。 - [NuxtJSとの併用](/docs/guides/tech/with-nuxtjs.md): NuxtJSプロジェクトでFSDを実装することは可能ですが、NuxtJSのプロジェクト構造要件とFSDの原則の違いにより、以下の2点でコンフリクトが発生してしまいます。 - [React Queryとの併用](/docs/guides/tech/with-react-query.md): キーをどこに置くか問題 - [SvelteKitとの併用](/docs/guides/tech/with-sveltekit.md): SvelteKitプロジェクトでFSDを実装することは可能ですが、SvelteKitのプロジェクト構造要件とFSDの原則の違いにより、以下の2点でコンフリクトが発生してしまいます。 - [Docs for LLMs](/docs/llms.md): This page provides links and guidance for LLM crawlers. - [レイヤー](/docs/reference/layers.md): レイヤーは、Feature-Sliced Designにおける組織階層の最初のレベルです。その目的は、コードを責任の程度やアプリ内の他のモジュールへの依存度に基づいて分離することです。各レイヤーは、コードにどれだけの責任を割り当てるべきかを判断するための特別な意味を持っています。 - [公開API](/docs/reference/public-api.md): 公開APIは、モジュールのグループ(スライスなど)とそれを使用するコードとの間の契約です。また、特定のオブジェクトへのアクセスを制限し、その公開APIを通じてのみアクセスを許可します。 - [スライスとセグメント](/docs/reference/slices-segments.md): スライス - [Feature-Sliced Design](/index.md): Architectural methodology for frontend projects --- # Full Documentation Content v2 ![](/documentation/ja/assets/ideal-img/tiny-bunny.dd60f55.640.png) Tiny Bunny Mini Game Mini-game "21 points" in the universe of the visual novel "Tiny Bunny". reactredux-toolkittypescript [Website](https://sanua356.github.io/tiny-bunny/)[Source](https://github.com/sanua356/tiny-bunny) --- # 🧭 ナビゲーション ## 古いリンク ドキュメントの再構成後、一部の記事リンクが変更されました。以下に探しているページが見つかるかもしれません。 互換性のために古いリンクからのリダイレクトがあります ### 🚀 Get Started ⚡️ Simplified and merged [Tutorial](/documentation/ja/docs/get-started/tutorial.md) [**old**:](/documentation/ja/docs/get-started/tutorial.md) [/docs/get-started/quick-start](/documentation/ja/docs/get-started/tutorial.md) [**new**: ](/documentation/ja/docs/get-started/tutorial.md) [/docs/get-started/tutorial](/documentation/ja/docs/get-started/tutorial.md) [Basics](/documentation/ja/docs/get-started/overview.md) [**old**:](/documentation/ja/docs/get-started/overview.md) [/docs/get-started/basics](/documentation/ja/docs/get-started/overview.md) [**new**: ](/documentation/ja/docs/get-started/overview.md) [/docs/get-started/overview](/documentation/ja/docs/get-started/overview.md) [Decompose Cheatsheet](/documentation/ja/docs/get-started/cheatsheet.md) [**old**:](/documentation/ja/docs/get-started/cheatsheet.md) [/docs/get-started/tutorial/decompose; /docs/get-started/tutorial/design-mockup; /docs/get-started/onboard/cheatsheet](/documentation/ja/docs/get-started/cheatsheet.md) [**new**: ](/documentation/ja/docs/get-started/cheatsheet.md) [/docs/get-started/cheatsheet](/documentation/ja/docs/get-started/cheatsheet.md) ### 🍰 Alternatives ⚡️ Moved and merged to /about/alternatives as advanced materials [Architecture approaches alternatives](/documentation/ja/docs/about/alternatives.md) [**old**:](/documentation/ja/docs/about/alternatives.md) [/docs/about/alternatives/big-ball-of-mud; /docs/about/alternatives/design-principles; /docs/about/alternatives/ddd; /docs/about/alternatives/clean-architecture; /docs/about/alternatives/frameworks; /docs/about/alternatives/atomic-design; /docs/about/alternatives/smart-dumb-components; /docs/about/alternatives/feature-driven](/documentation/ja/docs/about/alternatives.md) [**new**: ](/documentation/ja/docs/about/alternatives.md) [/docs/about/alternatives](/documentation/ja/docs/about/alternatives.md) ### 🍰 Promote & Understanding ⚡️ Moved to /about as advanced materials [Knowledge types](/documentation/ja/docs/about/understanding/knowledge-types.md) [**old**:](/documentation/ja/docs/about/understanding/knowledge-types.md) [/docs/reference/knowledge-types](/documentation/ja/docs/about/understanding/knowledge-types.md) [**new**: ](/documentation/ja/docs/about/understanding/knowledge-types.md) [/docs/about/understanding/knowledge-types](/documentation/ja/docs/about/understanding/knowledge-types.md) [Needs driven](/documentation/ja/docs/about/understanding/needs-driven.md) [**old**:](/documentation/ja/docs/about/understanding/needs-driven.md) [/docs/concepts/needs-driven](/documentation/ja/docs/about/understanding/needs-driven.md) [**new**: ](/documentation/ja/docs/about/understanding/needs-driven.md) [/docs/about/understanding/needs-driven](/documentation/ja/docs/about/understanding/needs-driven.md) [About architecture](/documentation/ja/docs/about/understanding/architecture.md) [**old**:](/documentation/ja/docs/about/understanding/architecture.md) [/docs/concepts/architecture](/documentation/ja/docs/about/understanding/architecture.md) [**new**: ](/documentation/ja/docs/about/understanding/architecture.md) [/docs/about/understanding/architecture](/documentation/ja/docs/about/understanding/architecture.md) [Naming adaptability](/documentation/ja/docs/about/understanding/naming.md) [**old**:](/documentation/ja/docs/about/understanding/naming.md) [/docs/concepts/naming-adaptability](/documentation/ja/docs/about/understanding/naming.md) [**new**: ](/documentation/ja/docs/about/understanding/naming.md) [/docs/about/understanding/naming](/documentation/ja/docs/about/understanding/naming.md) [Signals of architecture](/documentation/ja/docs/about/understanding/signals.md) [**old**:](/documentation/ja/docs/about/understanding/signals.md) [/docs/concepts/signals](/documentation/ja/docs/about/understanding/signals.md) [**new**: ](/documentation/ja/docs/about/understanding/signals.md) [/docs/about/understanding/signals](/documentation/ja/docs/about/understanding/signals.md) [Abstractions of architecture](/documentation/ja/docs/about/understanding/abstractions.md) [**old**:](/documentation/ja/docs/about/understanding/abstractions.md) [/docs/concepts/abstractions](/documentation/ja/docs/about/understanding/abstractions.md) [**new**: ](/documentation/ja/docs/about/understanding/abstractions.md) [/docs/about/understanding/abstractions](/documentation/ja/docs/about/understanding/abstractions.md) ### 📚 Reference guidelines (isolation & units) ⚡️ Moved to /reference as theoretical materials (old concepts) [Decouple of entities](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [**old**:](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [/docs/concepts/decouple-entities](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [Low Coupling & High Cohesion](/documentation/ja/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [**old**:](/documentation/ja/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [/docs/concepts/low-coupling](/documentation/ja/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [**new**: ](/documentation/ja/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [/docs/reference/slices-segments#zero-coupling-high-cohesion](/documentation/ja/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [Cross-communication](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [**old**:](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [/docs/concepts/cross-communication](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [App splitting](/documentation/ja/docs/reference/layers.md) [**old**:](/documentation/ja/docs/reference/layers.md) [/docs/concepts/app-splitting](/documentation/ja/docs/reference/layers.md) [**new**: ](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers](/documentation/ja/docs/reference/layers.md) [Decomposition](/documentation/ja/docs/reference/layers.md) [**old**:](/documentation/ja/docs/reference/layers.md) [/docs/reference/units/decomposition](/documentation/ja/docs/reference/layers.md) [**new**: ](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers](/documentation/ja/docs/reference/layers.md) [Units](/documentation/ja/docs/reference/layers.md) [**old**:](/documentation/ja/docs/reference/layers.md) [/docs/reference/units](/documentation/ja/docs/reference/layers.md) [**new**: ](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers](/documentation/ja/docs/reference/layers.md) [Layers](/documentation/ja/docs/reference/layers.md) [**old**:](/documentation/ja/docs/reference/layers.md) [/docs/reference/units/layers](/documentation/ja/docs/reference/layers.md) [**new**: ](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers](/documentation/ja/docs/reference/layers.md) [Layer overview](/documentation/ja/docs/reference/layers.md) [**old**:](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers/overview](/documentation/ja/docs/reference/layers.md) [**new**: ](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers](/documentation/ja/docs/reference/layers.md) [App layer](/documentation/ja/docs/reference/layers.md) [**old**:](/documentation/ja/docs/reference/layers.md) [/docs/reference/units/layers/app](/documentation/ja/docs/reference/layers.md) [**new**: ](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers](/documentation/ja/docs/reference/layers.md) [Processes layer](/documentation/ja/docs/reference/layers.md) [**old**:](/documentation/ja/docs/reference/layers.md) [/docs/reference/units/layers/processes](/documentation/ja/docs/reference/layers.md) [**new**: ](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers](/documentation/ja/docs/reference/layers.md) [Pages layer](/documentation/ja/docs/reference/layers.md) [**old**:](/documentation/ja/docs/reference/layers.md) [/docs/reference/units/layers/pages](/documentation/ja/docs/reference/layers.md) [**new**: ](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers](/documentation/ja/docs/reference/layers.md) [Widgets layer](/documentation/ja/docs/reference/layers.md) [**old**:](/documentation/ja/docs/reference/layers.md) [/docs/reference/units/layers/widgets](/documentation/ja/docs/reference/layers.md) [**new**: ](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers](/documentation/ja/docs/reference/layers.md) [Widgets layer](/documentation/ja/docs/reference/layers.md) [**old**:](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers/widgets](/documentation/ja/docs/reference/layers.md) [**new**: ](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers](/documentation/ja/docs/reference/layers.md) [Features layer](/documentation/ja/docs/reference/layers.md) [**old**:](/documentation/ja/docs/reference/layers.md) [/docs/reference/units/layers/features](/documentation/ja/docs/reference/layers.md) [**new**: ](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers](/documentation/ja/docs/reference/layers.md) [Entities layer](/documentation/ja/docs/reference/layers.md) [**old**:](/documentation/ja/docs/reference/layers.md) [/docs/reference/units/layers/entities](/documentation/ja/docs/reference/layers.md) [**new**: ](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers](/documentation/ja/docs/reference/layers.md) [Shared layer](/documentation/ja/docs/reference/layers.md) [**old**:](/documentation/ja/docs/reference/layers.md) [/docs/reference/units/layers/shared](/documentation/ja/docs/reference/layers.md) [**new**: ](/documentation/ja/docs/reference/layers.md) [/docs/reference/layers](/documentation/ja/docs/reference/layers.md) [Segments](/documentation/ja/docs/reference/slices-segments.md) [**old**:](/documentation/ja/docs/reference/slices-segments.md) [/docs/reference/units/segments](/documentation/ja/docs/reference/slices-segments.md) [**new**: ](/documentation/ja/docs/reference/slices-segments.md) [/docs/reference/slices-segments](/documentation/ja/docs/reference/slices-segments.md) ### 🎯 Bad Practices handbook ⚡️ Moved to /guides as practice materials [Cross-imports](/documentation/ja/docs/guides/issues/cross-imports.md) [**old**:](/documentation/ja/docs/guides/issues/cross-imports.md) [/docs/concepts/issues/cross-imports](/documentation/ja/docs/guides/issues/cross-imports.md) [**new**: ](/documentation/ja/docs/guides/issues/cross-imports.md) [/docs/guides/issues/cross-imports](/documentation/ja/docs/guides/issues/cross-imports.md) [Desegmented](/documentation/ja/docs/guides/issues/desegmented.md) [**old**:](/documentation/ja/docs/guides/issues/desegmented.md) [/docs/concepts/issues/desegmented](/documentation/ja/docs/guides/issues/desegmented.md) [**new**: ](/documentation/ja/docs/guides/issues/desegmented.md) [/docs/guides/issues/desegmented](/documentation/ja/docs/guides/issues/desegmented.md) [Routes](/documentation/ja/docs/guides/issues/routes.md) [**old**:](/documentation/ja/docs/guides/issues/routes.md) [/docs/concepts/issues/routes](/documentation/ja/docs/guides/issues/routes.md) [**new**: ](/documentation/ja/docs/guides/issues/routes.md) [/docs/guides/issues/routes](/documentation/ja/docs/guides/issues/routes.md) ### 🎯 Examples ⚡️ Grouped and simplified into /guides/examples as practical examples [Viewer logic](/documentation/ja/docs/guides/examples/auth.md) [**old**:](/documentation/ja/docs/guides/examples/auth.md) [/docs/guides/examples/viewer](/documentation/ja/docs/guides/examples/auth.md) [**new**: ](/documentation/ja/docs/guides/examples/auth.md) [/docs/guides/examples/auth](/documentation/ja/docs/guides/examples/auth.md) [Monorepo](/documentation/ja/docs/guides/examples/monorepo.md) [**old**:](/documentation/ja/docs/guides/examples/monorepo.md) [/docs/guides/monorepo](/documentation/ja/docs/guides/examples/monorepo.md) [**new**: ](/documentation/ja/docs/guides/examples/monorepo.md) [/docs/guides/examples/monorepo](/documentation/ja/docs/guides/examples/monorepo.md) [White Labels](/documentation/ja/docs/guides/examples/white-labels.md) [**old**:](/documentation/ja/docs/guides/examples/white-labels.md) [/docs/guides/white-labels](/documentation/ja/docs/guides/examples/white-labels.md) [**new**: ](/documentation/ja/docs/guides/examples/white-labels.md) [/docs/guides/examples/white-labels](/documentation/ja/docs/guides/examples/white-labels.md) ### 🎯 Migration ⚡️ Grouped and simplified into /guides/migration as migration guidelines [Migration from V1](/documentation/ja/docs/guides/migration/from-v1.md) [**old**:](/documentation/ja/docs/guides/migration/from-v1.md) [/docs/guides/migration-from-v1](/documentation/ja/docs/guides/migration/from-v1.md) [**new**: ](/documentation/ja/docs/guides/migration/from-v1.md) [/docs/guides/migration/from-v1](/documentation/ja/docs/guides/migration/from-v1.md) [Migration from Legacy](/documentation/ja/docs/guides/migration/from-custom.md) [**old**:](/documentation/ja/docs/guides/migration/from-custom.md) [/docs/guides/migration-from-legacy](/documentation/ja/docs/guides/migration/from-custom.md) [**new**: ](/documentation/ja/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-custom](/documentation/ja/docs/guides/migration/from-custom.md) ### 🎯 Tech ⚡️ Grouped into /guides/tech as tech-specific usage guidelines [Usage with NextJS](/documentation/ja/docs/guides/tech/with-nextjs.md) [**old**:](/documentation/ja/docs/guides/tech/with-nextjs.md) [/docs/guides/usage-with-nextjs](/documentation/ja/docs/guides/tech/with-nextjs.md) [**new**: ](/documentation/ja/docs/guides/tech/with-nextjs.md) [/docs/guides/tech/with-nextjs](/documentation/ja/docs/guides/tech/with-nextjs.md) ### Rename 'legacy' to 'custom' ⚡️ 'Legacy' is derogatory, we don't get to call people's projects legacy [Rename 'legacy' to custom](/documentation/ja/docs/guides/migration/from-custom.md) [**old**:](/documentation/ja/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-legacy](/documentation/ja/docs/guides/migration/from-custom.md) [**new**: ](/documentation/ja/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-custom](/documentation/ja/docs/guides/migration/from-custom.md) ### Deduplication of Reference ⚡️ Cleaned up the Reference section and deduplicated the material [Isolation of modules](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [**old**:](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/isolation](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/documentation/ja/docs/reference/layers.md#import-rule-on-layers) --- [メインコンテンツにスキップ](#__docusaurus_skipToContent_fallback) [![logo](/documentation/ja/img/brand/logo-primary.png)![logo](/documentation/ja/img/brand/logo-primary.png)](/documentation/ja/.md) [****](/documentation/ja/.md)[📖 ドキュメント](/documentation/ja/docs/get-started/overview.md)[💫 コミュニティ](/documentation/ja/community.md)[📝 ブログ](/documentation/ja/blog)[🛠 実装例](/documentation/ja/examples.md) [v2.1](/documentation/ja/docs/get-started/overview.md) * [v2.1](/documentation/ja/docs/get-started/overview.md) * [v1.0](https://feature-sliced.github.io/featureslices.dev/v1.0.html) * [v0.1](https://feature-sliced.github.io/featureslices.dev/v0.1.html) * [feature-driven](https://github.com/feature-sliced/documentation/tree/rc/feature-driven) * [All versions](/documentation/ja/versions.md) [日本語](#) * [Русский](/documentation/ru/search) * [English](/documentation/search) * [O'zbekcha](/documentation/uz/search) * [한국어](/documentation/kr/search) * [日本語](/documentation/ja/search.md) * [Help Us Translate](https://github.com/feature-sliced/documentation/issues/244) [](https://discord.gg/S8MzWTUsmp)[](https://github.com/feature-sliced/documentation) 検索 # ドキュメントを検索 検索するキーワードを入力してください [](https://www.algolia.com/) 仕様 * [ドキュメント](/documentation/ja/docs/get-started/overview.md) * [コミュニティ](/documentation/ja/community.md) * [ヘルプ](/documentation/ja/nav.md) * [ディスカッション](https://github.com/feature-sliced/documentation/discussions) コミュニティ * [Discord](https://discord.gg/S8MzWTUsmp) * [Telegram (RU)](https://t.me/feature_sliced) * [X](https://twitter.com/feature_sliced) * [Open Collective](https://opencollective.com/feature-sliced) * [YouTube](https://www.youtube.com/c/FeatureSlicedDesign) その他 * [GitHub](https://github.com/feature-sliced) * [Contribution Guide](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md) * [ライセンス](https://github.com/feature-sliced/documentation/blob/master/LICENSE) * [Docs for LLMs](/documentation/ja/docs/llms.md) [![Feature-Sliced Design - Architectural methodology for frontend projects](/documentation/ja/img/brand/logo-primary.png)![Feature-Sliced Design - Architectural methodology for frontend projects](/documentation/ja/img/brand/logo-primary.png)](https://github.com/feature-sliced) Copyright © 2018-2025 Feature-Sliced Design --- # Feature-Sliced Designのバージョン ### Feature-Sliced Design v2.1 (Current) ここで現在公開されているバージョンのドキュメントを見つけることができます | v2.1 | [Release Notes](https://github.com/feature-sliced/documentation/releases/tag/v2.1) | [Documentation](/documentation/ja/docs/get-started/overview.md) | [Migration from v1](/documentation/ja/docs/guides/migration/from-v1.md) | [Migration from v2.0](/documentation/ja/docs/guides/migration/from-v1.md) | | ---- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------- | ### Feature Slices v1 (Legacy) ここで古いバージョンのドキュメントを見つけることができます | v1.0 | [Documentation](https://feature-sliced.github.io/featureslices.dev/v1.0.html) | | ---- | ----------------------------------------------------------------------------- | | v0.1 | [Documentation](https://feature-sliced.github.io/featureslices.dev/v0.1.html) | ### Feature Driven (Legacy) ここで古いバージョンのドキュメントを見つけることができます | v0.1 | [Documentation](https://github.com/feature-sliced/documentation/tree/rc/feature-driven) | | ------------- | --------------------------------------------------------------------------------------- | | Example (kof) | [Github](https://github.com/kof/feature-driven-architecture) | --- # 💫 Community Community resources, additional materials ## Main[​](#main "この見出しへの直接リンク") [Awesome Resources](https://github.com/feature-sliced/awesome) [A curated list of awesome FSD videos, articles, packages](https://github.com/feature-sliced/awesome) [Team](/documentation/ja/community/team.md) [Core-team, Champions, Contributors, Companies](/documentation/ja/community/team.md) [Brandbook](/documentation/ja/docs/branding.md) [Recommendations for FSD's branding usage](/documentation/ja/docs/branding.md) [Contributing](#) [HowTo, Workflow, Support](#) --- # Team WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/192) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Core-team[​](#core-team "この見出しへの直接リンク") ### Champions[​](#champions "この見出しへの直接リンク") ## Contributors[​](#contributors "この見出しへの直接リンク") ## Companies[​](#companies "この見出しへの直接リンク") --- # 代替案 WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/62) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## ビッグボールオブマッド[​](#ビッグボールオブマッド "この見出しへの直接リンク") WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/258) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* * [(記事) DDD - Big Ball of mud](https://thedomaindrivendesign.io/big-ball-of-mud/) ## スマート&ダムコンポーネント[​](#スマートダムコンポーネント "この見出しへの直接リンク") WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/214) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* * [(記事) Dan Abramov - Presentational and Container Components (TLDR: 非推奨)](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) ## デザイン原則[​](#デザイン原則 "この見出しへの直接リンク") WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/59) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## DDD[​](#ddd "この見出しへの直接リンク") WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/1) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## 参照[​](#see-also "この見出しへの直接リンク") * [(記事) DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/) ## クリーンアーキテクチャ[​](#クリーンアーキテクチャ "この見出しへの直接リンク") WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/165) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* * [(記事) DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/) ## フレームワーク[​](#フレームワーク "この見出しへの直接リンク") WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/58) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* * [(記事) FSDの作成理由 (フレームワークに関する断片)](/documentation/ja/docs/about/motivation.md) ## Atomic Design[​](#atomic-design "この見出しへの直接リンク") ### これは何か?[​](#これは何か "この見出しへの直接リンク") アトミックデザインでは、責任の範囲が標準化された層に分かれています。 アトミックデザインは**5つの層**に分かれます(上から下へ)。 1. `pages` - FSDの`pages`層と同様の目的を持つ。 2. `templates` - コンテンツに依存しないページの構造を定義するコンポーネント。 3. `organisms` - ビジネスロジックを持つ分子から構成されるモジュール。 4. `molecules` - 通常、ビジネスロジックを持たないより複雑なコンポーネント。 5. `atoms` - ビジネスロジックを持たないUIコンポーネント。 同じ層のモジュールは、FSDのように下の層のモジュールとだけ相互作用しています。 つまり、分子(molecule)は原子(atom)から構築され、生命体(organism)は分子から、テンプレート(template)は生命体から、ページ(page)はテンプレートから構築されます。 また、アトミックデザインはモジュール内での**公開API**の使用を前提としています。 ### フロントエンドでの適用性[​](#フロントエンドでの適用性 "この見出しへの直接リンク") アトミックデザインはプロジェクトで比較的よく見られます。アトミックデザインは、開発者の間というより、ウェブデザイナーの間で人気です。ウェブデザイナーは、スケーラブルでメンテナンスしやすいデザインを作成するためにアトミックデザインをよく使用しています。 開発では、アトミックデザインは他のアーキテクチャ設計方法論と混合されることがよくあります。 しかし、アトミックデザインはUIコンポーネントとその構成に焦点を当てているため、 アーキテクチャ内でビジネスロジックを実装する問題が発生してしまいます。 問題は、アトミックデザインがビジネスロジックのための明確な責任レベルを提供していないため、 ビジネスロジックがさまざまなコンポーネントやレベルに分散され、メンテナンスやテストが複雑になることです。 ビジネスロジックは曖昧になり、責任の明確な分離が困難になり、コードがモジュール化されず再利用可能でなくなります。 ### FSDとの統合[​](#fsdとの統合 "この見出しへの直接リンク") FSDの文脈では、アトミックデザインのいくつかの要素を使用して柔軟でスケーラブルなUIコンポーネントを作成することができます。 `atoms`と`molecules`の層は、FSDの`shared/ui`に実装でき、基本的なUI要素の再利用とメンテナンスを簡素化しています。 ``` ├── shared │   ├── ui  │   │   ├── atoms │   │   ├── molecules │   ... ``` FSDとアトミックデザインの比較は、両方の設計方法論がモジュール性と再利用性を目指していることを示していますが、 異なる側面に焦点を当てています。アトミックデザインは視覚的コンポーネントとその構成に焦点を当てています。 FSDはアプリケーションの機能を独立したモジュールに分割し、それらの相互関係に焦点を当てています。 * [Atomic Design](https://atomicdesign.bradfrost.com/table-of-contents/) * [(動画) Atomic Design: What is it and why is it important?](https://youtu.be/Yi-A20x2dcA) ## Feature Driven[​](#feature-driven "この見出しへの直接リンク") WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/219) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* * [(講演) Feature Driven Architecture - Oleg Isonen](https://youtu.be/BWAeYuWFHhs) * [Feature Driven-Short specification (from the point of view of FSD)](https://github.com/feature-sliced/documentation/tree/rc/feature-driven) --- # ミッション ここでは、私たちがFSD方法論を開発する際に従う方法論適用の制限と目標について説明します。 * 私たちは、目標をイデオロギーとシンプルさのバランスとして考えている * 私たちは、すべての人に適した銀の弾丸を作ることはできない **それでも、FSD方法論が広範な開発者にとって近く、アクセス可能であることを望んでいます。** ## 目標[​](#goals "この見出しへの直接リンク") ### 幅広い開発者に対する直感的な明確さ[​](#intuitive-clarity-for-a-wide-range-of-developers "この見出しへの直接リンク") FSD方法論は、プロジェクトチームの大部分にとってアクセス可能であるべきです。 *なぜなら、将来のすべてのツールがあっても、FSD方法論を理解できるのは経験豊富なシニアやリーダーだけでは不十分だからである* ### 日常的な問題の解決[​](#solving-everyday-problems "この見出しへの直接リンク") FSD方法論には、プロジェクト開発における日常的な問題の理由と解決策が示されるべきです。 また、開発者が*コミュニティーの経験に基づいた*アプローチを使用できるようにし、長年のアーキテクチャや開発の問題を回避できるようにするには、**FSD方法論はこれに関連するツール(CLI、リンター)を提供することも必要です。** > *@sergeysova: 想像してみてください。開発者が方法論に基づいてコードを書いているとき、開発者の直面している問題は10倍少なく発生しています。それは他の人々が多くの問題の解決策を考え出したから、可能になったのです。* ## 制限[​](#limitations "この見出しへの直接リンク") 私たちは*自分たちの見解を押し付けたくありません*が、同時に*多くの開発者の習慣が日々の開発の妨げになっていることを理解しています。* すべての開発者にはシステム設計と開発経験レベルが異なるため、**次のことを理解することが重要です。** * FSD方法論は、すべての開発者にとって、同時に非常にシンプルで、非常に明確にするのは不可能 > *@sergeysova: 一部の概念は、問題に直面し、解決に数年を費やさない限り、直感的に理解することはできない。* > > * *数学の例 — グラフ理論。* > * *物理の例 — 量子力学。* > * *プログラミングの例 — アプリケーションのアーキテクチャ。* * シンプルさ、拡張性は、実現可能であって望ましい ## 参照[​](#see-also "この見出しへの直接リンク") * [アーキテクチャの問題](/documentation/ja/docs/about/understanding/architecture.md#problems) --- # モチベーション **Feature-Sliced Design**の主なアイデアは、さまざまな開発者の経験を議論し、研究結果を統合することに基づいて、複雑で発展するプロジェクトの開発を容易にし、開発コストを削減することです。 明らかに、これは銀の弾丸ではなく、当然ながら、FSDには独自の[適用範囲の限界](/documentation/ja/docs/about/mission.md)があります。 ## 既存の解決策が不足している理由[​](#intuitive-clarity-for-a-wide-range-of-developers "この見出しへの直接リンク") > 通常、次のような議論があります。 > > * *「SOLID」、「KISS」、「YAGNI」、「DDD」、「GRASP」、「DRY」など、すでに確立された設計原則があるのに、なぜ新しい方法論が必要なのか?」* > * *「プロジェクトのすべての問題は、良いドキュメント、テスト、確立されたプロセスで解決できる」* > * *「すべての問題は、すべての開発者が上記のすべてに従えば解決される」* > * *「すでにすべてが考案されているから、あなたはそれを利用できないだけだ」* > * *{FRAMEWORK\_NAME}を使えば、すべてが解決される」* ### 原則だけでは不十分[​](#principles-alone-are-not-enough "この見出しへの直接リンク") **良いアーキテクチャを設計するためには、原則の存在だけでは不十分です。** すべての人が原則を完全に理解しているわけではありません。正しく原則を理解し、適用できる人はさらに少ないです。 *設計原則はあまりにも一般的であり、「スケーラブルで柔軟なアプリケーションの構造とアーキテクチャをどのように設計するか?」という具体的な質問に対する答えを提供していません。* ### プロセスは常に機能するわけではない[​](#processes-dont-always-work "この見出しへの直接リンク") *ドキュメント/テスト/プロセス*を使用するのは、確かに良いことですが、残念ながら、それに多くのコストをかけても、**アーキテクチャの問題や新しい人をプロジェクトに導入する問題を解決することは常にできるわけではありません。** * ドキュメントは、しばしば膨大で古くなってしまうので、各開発者のプロジェクトへの参加時間はあまり短縮されない。 * 誰もが同じようにアーキテクチャを理解しているかを常に監視することは、膨大なリソースを必要とする。 * bus-factorも忘れないようにしましょう。 ### 既存のフレームワークはどこでも適用できるわけではない[​](#existing-frameworks-cannot-be-applied-everywhere "この見出しへの直接リンク") * 既存の解決策は通常、高い参入障壁があるため、新しい開発者を見つけるのが難しい。 * ほとんどの場合、技術の選択はプロジェクトの深刻な問題が発生する前に決定されているため、**技術に依存せずに**、すでにあるもので作業をすることができなければならない。 > Q: *「私のプロジェクトでは`React/Vue/Redux/Effector/Mobx/{YOUR_TECH}`を使っていますが、エンティティの構造とそれらの間の関係をどのように構築すればよいでしょうか?」* ### 結果として[​](#as-a-result "この見出しへの直接リンク") 「雪の結晶」のようにユニークなプロジェクトが得られ、それぞれが従業員の長期的な関与を必要とし、他のプロジェクトではほとんど適用できない知識を必要とします。 > @sergeysova: *これは、現在のフロントエンド開発の状況そのものであり、各リーダーがさまざまなアーキテクチャやプロジェクトの構造を考案しているが、これらの構造が時間の試練に耐えるかどうかは不明であり、最終的にはリーダー以外の人がプロジェクトを発展させることができるのは最大で2人であり、新しい開発者を再び入れる必要がある。* ## 開発者にとっての方法論の必要性[​](#why-do-developers-need-the-methodology "この見出しへの直接リンク") ### アーキテクチャの問題ではなくビジネス機能に集中するため[​](#focus-on-business-features-not-on-architecture-problems "この見出しへの直接リンク") FSDは、スケーラブルで柔軟なアーキテクチャの設計にかかるリソースを節約し、開発者の注意を主要な機能開発に向けることを可能にしています。同時に、プロジェクトごとにアーキテクチャの解決策も標準化されます。 *別の問題は、FSDがコミュニティの信頼を得る必要があることです。そうすれば、開発者は自分のプロジェクトの問題を解決する際に、与えられた時間内にFSDを理解し、信頼することができます。* ### 経験に基づく解決策[​](#an-experience-proven-solution "この見出しへの直接リンク") FSDは、*複雑なビジネスロジックの設計における経験に基づく解決策を目指す開発者*を対象としています。 *ただし、FSDは、全体としてベストプラクティスのセット、または特定の問題やケースに関する記事一覧です。したがって、開発や設計の問題に直面する他の開発者にも役立てます。* ### プロジェクトの健康[​](#project-health "この見出しへの直接リンク") FSDは、*プロジェクトの問題を事前に解決し、追跡することを可能にし、膨大なリソースを必要としません。* **技術的負債は通常、時間とともに蓄積され、その解決の責任はリーダーとチームの両方にあります。** FSDは、*スケーリングやプロジェクトの発展における潜在的な問題を事前に警告することを可能にしています。* ## ビジネスにとってのFSD方法論の必要性[​](#why-does-a-business-need-a-methodology "この見出しへの直接リンク") ### 迅速なオンボーディング[​](#fast-onboarding "この見出しへの直接リンク") FSDを使用すると、**すでにこのアプローチに慣れている人をプロジェクトに雇うことができ、再教育する必要がありません。** *人々はより早くプロジェクトに慣れ、貢献し始め、次のプロジェクトのイテレーションで人を見つけるための追加の保証が得られます。* ### 経験に基づく解決策[​](#an-experience-proven-solution-1 "この見出しへの直接リンク") ビジネスは、プロジェクトの発展における大部分の問題を解決するフレームワーク/解決策を得たいと考えています。FSDにより、ビジネスは*システムの開発中に発生するほとんどの問題に対する解決策を得ることができます。* ### プロジェクトのさまざまな段階への適用性[​](#applicability-for-different-stages-of-the-project "この見出しへの直接リンク") FSDは、*プロジェクトのサポートと発展の段階でも、MVPの段階でもプロジェクトに利益をもたらすことができます。* はい、MVPでは通常、*機能が重要であり、将来のアーキテクチャは重要ではありません*。しかし、限られた時間の中で、方法論のベストプラクティスを知っていることで、*少ないコストで済む*ことができ、MVPバージョンのシステムを設計する際に合理的な妥協を見つけることができます(無計画に機能を追加するよりも)。 *テストについても同じことが言えます。* ## 私たちの方法論が必要ない場合[​](#when-is-our-methodology-not-needed "この見出しへの直接リンク") * プロジェクトが短期間しか存続しない場合 * プロジェクトがサポートされるアーキテクチャを必要としない場合 * ビジネスがコードベースと機能の提供速度の関連性を認識しない場合 * ビジネスができるだけ早く注文を完了することを重視し、さらなるサポートを求めない場合 ### ビジネスの規模[​](#business-size "この見出しへの直接リンク") * **小規模ビジネス** - 通常、迅速で即効性のある解決策を必要とします。ビジネスは、成長する(少なくとも中規模に達する)と、顧客が継続的にサービスなどを利用するためには、開発される解決策の品質と安定性に時間をかける必要があることを理解し始めます。 * **中規模ビジネス** - 通常、開発のすべての問題を理解しており、たとえ機能をできるだけ早くリリースしたい場合でも、品質の改善、リファクタリング、テスト(そしてもちろん、拡張可能なアーキテクチャ)に時間をかけます。 * **大規模ビジネス** - 通常、すでに広範なオーディエンスを持ち、従業員の数も多く、独自のプラクティスのセットを持っているため、他のアプローチを採用するアイデアはあまり浮かびません。 ## 目標[​](#plans "この見出しへの直接リンク") 主要な目標の大部分は[ここに記載されています](/documentation/ja/docs/about/mission.md#goals)が、今後のFSD方法論に対する私たちの期待についても話しておく必要があります。 ### 経験の統合[​](#combining-experience "この見出しへの直接リンク") 現在、私たちは`core-team`のさまざまな経験を統合し、実践に基づいた方法論を得ることを目指しています。 もちろん、最終的にはAngular 3.0のようなものを得るかもしれませんが、ここで最も重要なのは、**複雑なシステムのアーキテクチャ設計の問題を探求することです。** *そして、現在のFSD方法論のバージョンに対して不満があることは確かですが、私たちはコミュニティの経験も考慮しながら、共通の努力で統一的かつ最適な解決策に到達したいと考えています。* ### 仕様外の生活[​](#life-outside-the-specification "この見出しへの直接リンク") すべてがうまくいけば、FSDは仕様やツールキットに限定されることはありません。 * 講演や記事があるかもしれない。 * FSD方法論に従って書かれたプロジェクトの他の技術への移行のための`CODE_MODEs`があるかもしれない。 * 最終的には、大規模な技術的解決策のメンテイナーに到達できるかもしれない。 * *特にReactに関しては、他のフレームワークと比較して、これは主な問題である。なぜなら、特定の問題を解決する方法を示さないからである。* ## 参照[​](#see-also "この見出しへの直接リンク") * [方法論の使命について:目標と制限](/documentation/ja/docs/about/mission.md) * [プロジェクトにおける知識の種類](/documentation/ja/docs/about/understanding/knowledge-types.md) --- # 会社での推進 WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/206) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # チームでの推進 WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/182) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # 統合の側面 **利点**: * [概要](/documentation/ja/docs/get-started/overview.md#advantages) * コードレビュー * オンボーディング **欠点**: * メンタル的な複雑さ * 高い参入障壁 * 「レイヤー地獄」 * 機能ベースのアプローチにおける典型的な問題 --- # 部分的な適用 WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/199) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # 抽象化 WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/186) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## 漏れのある抽象化の法則[​](#the-law-of-leaky-abstractions "この見出しへの直接リンク") ## なぜこんなに多くの抽象化があるのか[​](#why-are-there-so-many-abstractions "この見出しへの直接リンク") > 抽象化はプロジェクトの複雑さに対処するのに役立ちます。問題は、これらの抽象化がこのプロジェクトに特有のものになるのか、それともフロントエンドの特性に基づいて一般的な抽象化を導き出そうとするのかということです。 > アーキテクチャとアプリケーション全体は元々複雑であり、問題はその複雑さをどのように分配し、記述するかだけです。 --- # アーキテクチャ ## 問題[​](#problems "この見出しへの直接リンク") 通常、アーキテクチャについての議論は、プロジェクトの開発が何らかの問題で停滞しているときに持ち上がります。 ### バスファクターとオンボーディング[​](#バスファクターとオンボーディング "この見出しへの直接リンク") プロジェクトとそのアーキテクチャを理解しているのは限られた人々だけです。 **例:** * *「新しい人を開発に加えるのが難しい」* * *「問題があるたびに、各自が異なる回避策を持っている」* * *「この大きなモノリスの中で何が起こっているのか理解できない」* ### 暗黙的かつ制御されていない結果[​](#implicit-and-uncontrolled-consequences "この見出しへの直接リンク") 開発やリファクタリングにおいて多くの暗黙的な副作用が発生してしまいます(「すべてがすべてに依存している」)。 **例:** * *「フィーチャーが他のフィーチャーをインポートしている」* * *「あるページのストアを更新したら、別のページのフィーチャーが壊れた」* * *「ロジックがアプリ全体に散らばっていて、どこが始まりでどこが終わりかわからない」* ### 制御されていないロジックの再利用[​](#uncontrolled-reuse-of-logic "この見出しへの直接リンク") 既存のロジックを再利用したり修正したりするのが難しいです。 通常、2つの極端なケースがあります。 * 各モジュールごとにロジックを完全にゼロから書く(既存のコードベースに重複が生じる可能性がある) * すべてのモジュールを`shared`フォルダーに移動し、大きなモジュールの「ごみ屋敷」を作る(ほとんどが一箇所でしか使用されない) **例:** * *「プロジェクトに同じビジネスロジックの複数の実装があって、毎日その影響を受けている」* * *「プロジェクトには6つの異なるボタン/ポップアップコンポーネントがある」* * *「ヘルパー関数の「ごみ屋敷」」* ## 要件[​](#requirements "この見出しへの直接リンク") したがって、理想的なアーキテクチャに対する要求を提示するのは、理にかなっています。 注記 「簡単」と言われるところでは、「広範な開発者にとって相対的に簡単である」という意味です。なぜなら、[すべての人にとって完璧な解決策を作ることはできないからです](/documentation/ja/docs/about/mission.md#limitations)。 ### 明示性[​](#明示性 "この見出しへの直接リンク") * チームがプロジェクトとそのアーキテクチャを**簡単に習得し、説明できる**ようにする必要がある * 構造はプロジェクトの**ビジネス価値**を反映するべきである * **副作用と抽象化間の関係**が明示されるべきである * ユニークな実装を妨げず、**ロジックの重複を簡単に発見できる**ようにする必要がある * プロジェクト全体に**ロジックが散らばってはいけない** * 良好なアーキテクチャのために**あまりにも多くの異なる抽象化やルールが存在してはならない** ### 制御[​](#制御 "この見出しへの直接リンク") * 良好なアーキテクチャは**課題の解決や機能の導入を加速する**べきである * プロジェクトの開発を**制御**できる必要がある * コードを**拡張、修正、削除するのが簡単である**べきである * 機能の**分解と孤立性**が守られるべきである * システムの各コンポーネントは**簡単に交換可能で削除可能**であるべきである * *未来を予測することはできないから、[変更に最適化する必要はない](https://youtu.be/BWAeYuWFHhs?t=1631)* * *既存のコンテキストに基づいて、[削除に最適化する方が良い](https://youtu.be/BWAeYuWFHhs?t=1666)* ### 適応性[​](#適応性 "この見出しへの直接リンク") * 良好なアーキテクチャは、ほとんどのプロジェクトに適用可能であるべきである * *既存のインフラソリューションと共に* * *どの発展段階でも* * フレームワークやプラットフォームに依存してはいけない * プロジェクトとチームを簡単にスケールアップでき、開発の並行処理が可能である必要がある * 変化する要件や状況に適応するのが簡単であるべきである ## 関連情報[​](#see-also "この見出しへの直接リンク") * [(React Berlin Talk) Oleg Isonen - Feature Driven Architecture](https://youtu.be/BWAeYuWFHhs) * [(記事) プロジェクトのモジュール化について](https://alexmngn.medium.com/why-react-developers-should-modularize-their-applications-d26d381854c1) * [(記事) 関心の分離と機能に基づく構造について](https://ryanlanciaux.com/blog/2017/08/20/a-feature-based-approach-to-react-development/) --- # プロジェクトにおける知識の種類 どのプロジェクトにも以下の「知識の種類」が存在します。 * **基礎知識**
時間とともにあまり変わらない知識。例えばアルゴリズム、コンピュータサイエンス、プログラミング言語やそのAPIの動作メカニズムなど。 * **技術スタック**
プロジェクトで使用される技術的解決策のセットに関する知識。プログラミング言語、フレームワーク、ライブラリを含む。 * **プロジェクト知識**
現在のプロジェクトに特有であり、他のプロジェクトでは役に立たない知識。この知識は新しいチームメンバーが効果的にプロジェクトに貢献するために必要である。 注記 **Feature-Sliced Design**は「プロジェクト知識」への依存を減らし、より多くの責任を引き受け、新しいチームメンバーのオンボーディングを容易にすることを目指している。 --- # ネーミング 異なる開発者は異なる経験と背景を持っているため、同じエンティティが異なる名前で呼ばれることによって、チーム内で誤解が生じる可能性があります。例えば * 表示用のコンポーネントは「ui」、「components」、「ui-kit」、「views」などと呼ばれることがある。 * アプリケーション全体で再利用されるコードは「core」、「shared」、「app」などと呼ばれることがある。 * ビジネスロジックのコードは「store」、「model」、「state」などと呼ばれることがある。 ## Feature-Sliced Designにおけるネーミング[​](#naming-in-fsd "この見出しへの直接リンク") FSD設計方法論では、以下のような特定の用語が使用されます。 * 「app」、「process」、「page」、「feature」、「entity」、「shared」といった層の名前、 * 「ui」、「model」、「lib」、「api」、「config」といったセグメントの名前。 これらの用語を遵守することは、チームメンバーやプロジェクトに新しく参加する開発者の混乱を防ぐために非常に重要です。標準的な名称を使用することは、コミュニティに助けを求める際にも役立ちます。 ## 名前衝突[​](#when-can-naming-interfere "この見出しへの直接リンク") 名前衝突は、FSD設計方法論で使用される用語がビジネスで使用される用語と重なっている場合に発生する可能性があります。例えば * `FSD#process`と、アプリケーション内でモデル化されたプロセス、 * `FSD#page`と、マガジンのページ、 * `FSD#model`と、自動車モデル。 開発者がコード内で「プロセス」という言葉を見た場合、どのプロセスが指されているのかを理解するのに余分な時間を費やすことになってしまいます。このような**衝突は開発プロセスを妨げる場合があります**。 プロジェクトの用語集にFSD特有の用語が含まれている場合、これらの用語をチームや技術的に関心のない関係者と議論する際には特に注意が必要です。 チームとの効果的なコミュニケーションのためには、用語の前に「FSD」という略語を付けることをお勧めします。例えば、プロセスについて話すときは、「このプロセスをFSDのフィーチャー層に置くことができる」と言うことができます。 逆に、技術的でない関係者とのコミュニケーションでは、FSDの用語の使用を制限し、コードベースの内部構造に言及しない方が良いでしょう。 --- # ニーズの理解と課題の定義について TL;DR — *新しい機能が解決する目標を明確にできませんか?それとも、問題はタスク自体が明確にされていないことにありますか?**FSDは、問題の定義や目標を引き出す手助けをすることも目的にしています。*** — *プロジェクトは静的に存在するわけではなく、要件や機能は常に変化しています。プロジェクトは最初の要望のスナップショットのみに基づいて設計されているため、時間が経つにつれて、コードは混沌としてしまいます。**良いアーキテクチャの課題の一つは、変化する開発条件に対応できるようにすることです。*** ## なぜ?[​](#why "この見出しへの直接リンク") エンティティの明確な名前を選び、その構成要素を理解するためには、**コードが解決する課題を明確に理解する必要があります。** > *@sergeysova: 開発中、私たちは各エンティティや機能に、その意図や意味を明確に反映する名前を付けようとしている。* *課題を理解しなければ、重要なケースをカバーする正しいテストを書くことも、ユーザーに適切な場所でエラーを表示することもできず、単純にユーザーのフローを中断することにもなってしまいます。* ## どのような課題についての話?[​](#what-tasks-are-we-talking-about "この見出しへの直接リンク") フロントエンドは、エンドユーザーのためのアプリケーションやインターフェースを開発しているため、私たち開発者はその消費者の課題を解決しています。 私たちのもとに誰かが来るとき、**その人は自分の悩みを解決したり、ニーズを満たしたりしてほしいのです。** *マネージャーとアナリストの仕事はこのニーズを定義することです。開発者の仕事はウェブ開発の特性(接続の喪失、バックエンドのエラー、タイプミス、カーソルや指の操作ミス)を考慮して、そのニーズを実現することです。* **ユーザーが持ってきた目的こそが、開発者の課題です。** > *小さな解決された課題が、Feature-Sliced Designの設計方法論におけるfeatureではあります。プロジェクト課題のスコープを小さな目標に分割する必要があります。* ## これが開発にどのように影響するのか?[​](#how-does-this-affect-development "この見出しへの直接リンク") ### 課題(タスク)の分解[​](#task-decomposition "この見出しへの直接リンク") 開発者がタスクを実装し始めるとき、理解の簡素化とコードメンテナンスのために、**タスクを段階に分けます**。 * まずは、上位レベルのエンティティに分けて、それを実装する * 次に、これらのエンティティをより小さく分ける * そしてさらに続ける *エンティティを分解する過程で、開発者はそれに明確に意図を反映した名前を付ける必要があり、エンティティの一覧表を読む際にそのコードが解決する課題を理解するのに役立ちます。* この際、ユーザーの悩みを軽減したり、ニーズを実現したりするユーザーへの手助けをすることを忘れないように心がけましょう。 ### 課題の本質を理解する[​](#understanding-the-essence-of-the-task "この見出しへの直接リンク") エンティティに明確な名前を付けるためには、**開発者はその目的について十分に理解する必要があります。** * エンティティをどのように使用するつもりなのか * エンティティがユーザーの課題のどの部分を実現するのか、他にどこでこのエンティティを使用できるのか * などなど 結論を出すのは難しくありません。**開発者がFSD枠内でのエンティティの名前を考えているとき、コードを書く前に不十分に定義された課題を見つけることができます。** > どのようにエンティティに名前を付けるのか、もしそのエンティティが解決できる課題をよく理解していない場合、そもそもどうやって課題をエンティティに分解できるのか? ## どのように定義するのか?[​](#how-to-formulate-it "この見出しへの直接リンク") 機能によって解決される課題を定義するためには、その課題自体を理解する必要があります。これはプロジェクトマネージャーやアナリストの責任範囲です。 *FSD設計方法論は、開発者に対して、プロダクトマネージャーが注目すべき課題を示唆することしかできません。* > *@sergeysova: フロントエンドは、まず情報を表示するものである。どのコンポーネントも、まず何かを表示する。したがって、「ユーザーに何かを見せる」というタスクには実用的な価値がない。* 基本的なニーズや悩みを見つけたら、**あなたのプロダクトやサービスがどのようにユーザーの目標をサポートすることができるのかを考えます。** タスクトラッカーの新しいタスクは、ビジネスの課題を解決することを目的としており、ビジネスは同時にユーザーの課題を解決し、利益を上げようとしています。したがって、説明文に明記されていなくても、各タスクには特定の目標が含まれています。 ***開発者は、特定のタスクが追求する目的をはっきりと把握しておくべきです。***しかし、すべての会社がプロセスを完璧に構築できるわけではありません。 ## その利益は何か?[​](#and-what-is-the-benefit "この見出しへの直接リンク") では、プロセス全体を最初から最後まで見てみましょう。 ### 1. ユーザーの課題を理解する[​](#1-understanding-user-tasks "この見出しへの直接リンク") 開発者は、ユーザーの悩みとビジネスがその悩みをどのように解決するかを理解すると、ウェブ開発の特性によりビジネスには提供できない解決策を提案することができます。 > しかしもちろん、これは開発者が自分の行動や目的に無関心でない限り機能します。さもなければ、そもそもなぜFSDやアプローチが必要なのか?という疑問になってしまいます。 ### 2. 構造化と整理[​](#2-structuring-and-ordering "この見出しへの直接リンク") 課題を理解することで、**頭とコードの中で明確な構造が得られます。** ### 3. 機能とその構成要素を理解する[​](#3-understanding-the-feature-and-its-components "この見出しへの直接リンク") **1つの機能は、ユーザーにとって1つの有用な機能性です。** * 1つの機能に複数の機能性が実装されている場合、それは**境界の侵害**である。 * 機能は分割不可能で成長可能になる場合があるが、**それは悪くない。** * **悪い**のは、機能が「ユーザーにとってのビジネス価値は何か?」という質問に答えられないことである。 * 「オフィスの地図」という機能は存在できない。 * しかし、「地図上の会議室の予約」、「従業員の検索」、「作業場所の変更」は**存在可能である。** > *@sergeysova: 機能には、直接的にその機能を実現するコードだけが含まれるべきであり、余計な詳細や内部の解決策は含まれないべきである(理想的には)。* > *機能のコードを開くと、**そのタスクに関連するものだけが見える**。それ以上は必要ない。* ### 4. Profit[​](#4-profit "この見出しへの直接リンク") ビジネスはその方針を極めて稀にしか根本的に変えないため、**ビジネスのタスクをフロントエンドアプリケーションのコードに反映することは非常に大きな利点になれます。** *そうすれば、チームの新しいメンバーにそのコードが何をするのか、なぜ追加されたのかを説明する必要がなくなります。**すべては、すでにコードに反映されているビジネスのタスクを通じて説明されているからです。*** > [Domain Driven Developmentにおける「ビジネス言語」](https://thedomaindrivendesign.io/developing-the-ubiquitous-language) *** ## 現実に戻りましょう[​](#back-to-reality "この見出しへの直接リンク") ビジネスプロセスが明確な意味を持ち、設計段階で良い名前が付けられている場合、*その理解と論理をコードに移すことはそれほど問題ではありません。* **しかし実際には、**タスクや機能性は通常「過度に」反復的に進化し、(または)デザインを考える時間がありません。 **その結果、今日、機能は意味を持っていますが、1か月後にその機能を拡張する際には、プロジェクト全体を再構築する必要があるかもしれません。** > *開発者は未来の要望を考慮しながら2〜3ステップ先を考えようとしますが、自分の経験に行き詰まってしまいます。* > *経験豊富なエンジニアは通常、すぐに10ステップ先を見て、どの機能を分割するか、どの機能を他の機能と統合するかを理解しています。* > *しかし、経験上遭遇したことのないタスクが来ることもあり、その場合、どのように機能を適切に分解し、将来的に悲惨な結果を最小限に抑えるかを理解する手段がありません。* ## FSDの役割[​](#the-role-of-methodology "この見出しへの直接リンク") **FSDは、開発者の問題を解決する手助けをし、ユーザーの問題を解決するのを容易にしています。** 開発者のためだけに課題を解決することはありません。 しかし、開発者が自分の課題を解決するためには、**ユーザーの課題を理解する必要があります**。逆は成り立ちません。 ### FSDに対する要件[​](#methodology-requirements "この見出しへの直接リンク") 明らかになるのは、**Feature-Sliced Design**のために少なくとも2つの要件を定義する必要があるということです。 1. FSD方法論は**フィーチャー、プロセス、エンティティを作成する方法**を説明する必要がある。 * つまり、*それらの間でコードをどのように分割するか*を明確に説明する必要がある。これによりこれらのエンティティの命名も仕様に組み込まれるべきである。 2. FSD方法論は、アーキテクチャが**プロジェクトの変わりゆく要件にスムーズに対応できるようにするべき**である。 ## 関連情報[​](#see-also "この見出しへの直接リンク") * [(記事) "How to better organize your applications"](https://alexmngn.medium.com/how-to-better-organize-your-react-applications-2fd3ea1920f1) --- # アーキテクチャのシグナル WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/194) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # ブランドガイドライン FSDのビジュアルアイデンティティは、そのコアコンセプトである `Layered`、`Sliced self-contained parts`、`Parts & Compose`、`Segmented` に基づいています。しかし、私たちはFSDの哲学を反映し、簡単に認識できる美しいアイデンティティを目指しています。 **FSDのアイデンティティを「そのまま」変更せずに、私たちのアセットを使って快適にご利用ください。** このブランドガイドは、FSDのアイデンティティを正しく使用する手助けをします。 互換性 FSDは以前、[別のレガシーアイデンティティ](https://drive.google.com/drive/folders/11Y-3qZ_C9jOFoW2UbSp11YasOhw4yBdl?usp=sharing)を持っていました。古いデザインは、FSDの主要なコンセプトを反映していませんでした。また、これは粗いドラフトとして作成され、更新されるべきものでした。 ブランドの互換性と長期的な使用のために、私たちは2021年から2022年にかけて慎重にリブランディングに取り組みました。**FSDのアイデンティティを使用する際に自信を持てるように🍰** *古いアイデンティティではなく、最新のアイデンティティを使用してください!* ## 名前[​](#title "この見出しへの直接リンク") * ✅ **正しい:** `Feature-Sliced Design`、`FSD` * ❌ **間違っている:** `Feature-Sliced`、`Feature Sliced`、`FeatureSliced`、`feature-sliced`、`feature sliced`、`FS` ## 絵文字[​](#emojii "この見出しへの直接リンク") ケーキのイメージ 🍰 はFSDの主要なコンセプトをよく反映しているため、私たちのブランド絵文字として選ばれました。 > 例: *"🍰 フロントエンド用ののアーキテクチャデザイン設計方法論"* ## ロゴとカラーパレット[​](#logo--palettte "この見出しへの直接リンク") FSDには異なるコンテキスト用のいくつかのロゴバリエーションがありますが、**primary**の使用が推奨されます。 | | | | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------- | | テーマ | ロゴ (Ctrl/Cmd + Clickでダウンロード) | 使用法 | | primary
(#29BEDC, #517AED) | [![logo-primary](/documentation/ja/img/brand/logo-primary.png)](/documentation/ja/img/brand/logo-primary.png) | ほとんどの場合に推奨されます | | flat
(#3193FF) | [![logo-flat](/documentation/ja/img/brand/logo-flat.png)](/documentation/ja/img/brand/logo-flat.png) | 単色コンテキスト用 | | monochrome
(#FFF) | [![logo-monochrome](/documentation/ja/img/brand/logo-monochrome.png)](/documentation/ja/img/brand/logo-monochrome.png) | 白黒コンテキスト用 | | square
(#3193FF) | [![logo-square](/documentation/ja/img/brand/logo-square.png)](/documentation/ja/img/brand/logo-square.png) | 正方形サイズ用 | ## バナーとスキーム[​](#banners--schemes "この見出しへの直接リンク") [![banner-primary](/documentation/ja/img/brand/banner-primary.jpg)](/documentation/ja/img/brand/banner-primary.jpg) [![banner-monochrome](/documentation/ja/img/brand/banner-monochrome.jpg)](/documentation/ja/img/brand/banner-monochrome.jpg) ## ソーシャルプレビュー[​](#ソーシャルプレビュー "この見出しへの直接リンク") 作業中... ## プレゼンテーションテンプレート[​](#presentation-template "この見出しへの直接リンク") 作業中... ## 参照[​](#see-also "この見出しへの直接リンク") * [ディスカッション (github)](https://github.com/feature-sliced/documentation/discussions/399) * [リブランディングの歴史と参考資料 (figma)](https://www.figma.com/file/RPphccpoeasVB0lMpZwPVR/FSD-Brand?node-id=0%3A1) --- # 分解のチートシート インターフェースをレイヤーに分割する際の参考書として使用してください。以下にPDFバージョンもあり、印刷して枕の下に置いておくことができます。 ## レイヤーの選択[​](#choosing-a-layer "この見出しへの直接リンク") [PDFをダウンロード](/documentation/ja/assets/files/choosing-a-layer-en-12fdf3265c8fc4f6b58687352b81fce7.pdf) ![レイヤーの定義と自己チェックの質問](/documentation/ja/assets/images/choosing-a-layer-en-5b67f20bb921ba17d78a56c0dc7654a9.jpg) ## 例[​](#examples "この見出しへの直接リンク") ### ツイート[​](#ツイート "この見出しへの直接リンク") ![分解されたツイート](/documentation/ja/assets/images/decompose-twitter-7b9a50f879d763c49305b3bf0751ee35.png) ### GitHub[​](#github "この見出しへの直接リンク") ![分解されたGitHub](/documentation/ja/assets/images/decompose-github-a0eeb839a4b5ef5c480a73726a4451b0.jpg) ## 参照[​](#see-also "この見出しへの直接リンク") * [(記事) ロジックの分解におけるさまざまなアプローチ](https://www.pluralsight.com/resources/blog/guides/how-to-organize-your-react--redux-codebase) --- # FAQ 備考 質問は、[Discordコミュニティ](https://discord.gg/S8MzWTUsmp)、[GitHub Discussions](https://github.com/feature-sliced/documentation/discussions)、および[Telegramチャット](https://t.me/feature_sliced)で聞くことができます。 ### ツールキットやリンターはありますか?[​](#is-there-a-toolkit-or-a-linter "この見出しへの直接リンク") はい!CLI または IDE を通じてプロジェクトのアーキテクチャと [フォルダー ジェネレーター](https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools) をチェックするための [Steiger](https://github.com/feature-sliced/steiger) というリンターがあります。 ### ページのレイアウト/テンプレートはどこに保存すればよいですか?[​](#where-to-store-the-layouttemplate-of-pages "この見出しへの直接リンク") シンプルなレイアウトテンプレートが必要な場合は、`shared/ui`に保存できます。より上層のレイヤーを使用する必要がある場合、いくつかのオプションがあります。 * レイアウトが本当に必要ですか?レイアウトが数行で構成されている場合、各ページにコードを重複させる方が合理的です。 * レイアウトが必要な場合は、個別のウィジェットやページとして保存し、App層のルーター設定にそれらを組み合わせることができます。ネストされたルーティングも一つのオプションです。 ### フィーチャーとエンティティの違いは何ですか?[​](#what-is-the-difference-between-feature-and-entity "この見出しへの直接リンク") *エンティティ*はアプリケーションが扱う現実世界の概念です。*フィーチャー*はユーザーに実際の価値を提供するインタラクションであり、ユーザーがエンティティで行いたいことです。 詳細および例については、参考書セクションの[スライスについてのページ](/documentation/ja/docs/reference/layers.md#entities)を参照してください。 ### ページ/フィーチャー/エンティティを相互に埋め込むことはできますか?[​](#can-i-embed-pagesfeaturesentities-into-each-other "この見出しへの直接リンク") はい、しかし、この埋め込みはより上層のレイヤーで行う必要があります。例えば、ウィジェット内で両方のフィーチャーをインポートし、プロップス/子要素として一方のフィーチャーを他方に挿入することができます。 一方のフィーチャーを他方のフィーチャーからインポートすることはできません。これは[**レイヤーのインポートルール**](/documentation/ja/docs/reference/layers.md#import-rule-on-layers)で禁止されています。 ### Atomic Designはどうですか?[​](#what-about-atomic-design "この見出しへの直接リンク") 現在、アトミックデザインをFeature-Sliced Designと一緒に使用することを義務付けていませんが、禁止もしていません。 アトミックデザインは、モジュールの`ui`セグメントにうまく適用できます。 ### FSDに関する有用なリソース/記事などはありますか?[​](#are-there-any-useful-resourcesarticlesetc-about-fsd "この見出しへの直接リンク") はい! ### なぜFeature-Sliced Designが必要なのですか?[​](#why-do-i-need-feature-sliced-design "この見出しへの直接リンク") FSDは、プロジェクトの主要な価値を提供するコンポーネントの観点から、あなたとあなたのチームが迅速にプロジェクトを把握するのに役立ちます。標準化されたアーキテクチャは、オンボーディングを迅速化し、コード構造に関する議論を解決するのに役立ちます。FSDが作成された理由については、[モチベーション](/documentation/ja/docs/about/motivation.md)のページを参照してください。 ### 初心者の開発者にFSDのアーキテクチャ/設計方法論は必要ですか?[​](#does-a-novice-developer-need-an-architecturemethodology "この見出しへの直接リンク") おそらく必要です。 *通常、一人でプロジェクトを設計・開発する場合、すべてが順調に進みます。しかし、開発に中断が生じたり、新しい開発者がチームに加わると問題が発生します。* ### 認証コンテキストをどのように扱えばよいですか?[​](#how-do-i-work-with-the-authorization-context "この見出しへの直接リンク") [こちら](/documentation/ja/docs/guides/examples/auth.md)で回答しています。 --- # 概要 **Feature-Sliced Design** (FSD) とは、フロントエンドアプリケーションの設計方法論です。簡単に言えば、コードを整理するためのルールと規約の集大成です。FSDの主な目的は、ビジネス要件が絶えず変化する中で、プロジェクトをより理解しやすく、構造化されたものにすることです。 ルールのセットに加えて、FSDはツールチェーンでもあります。プロジェクトのアーキテクチャをチェックするための[リンター](https://github.com/feature-sliced/steiger)、CLIやIDEを通じた[フォルダージェネレーター](https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools)、および豊富な[実装例のコレクション](/documentation/ja/examples.md)があります。 ## FSDは私のプロジェクトに適しているのか?[​](#is-it-right-for-me "この見出しへの直接リンク") FSDは、あらゆる規模のプロジェクトやチームに導入できます。以下の場合、あなたのプロジェクトに適しています。 * **フロントエンド**開発での使用(ウェブサイト、モバイル/デスクトップアプリケーションのインターフェース作成など) * **アプリケーション**開発での使用(ライブラリ開発ではない) これだけです!使用するプログラミング言語、フレームワーク、状態管理ライブラリには制限がありません。尚、FSDを段階的に導入したり、モノレポで使用したり、アプリケーションをパッケージに分割し、それぞれにFSDを個別に導入することもできます! 既存のアーキテクチャからFSDに移行することを検討している場合は、現在のアーキテクチャがチームに**支障をきたしている**かどうかを確認してください。例えば、プロジェクトが大きくなりすぎて新機能の開発が効率的に行えない場合や、多くの新しいメンバーがチームに加わることが予想される場合です。現在のアーキテクチャが正常に機能している場合、変更する必要はないかもしれません。しかし、移行を決定した場合は、[移行セクション](/documentation/ja/docs/guides/migration/from-custom.md)の推奨事項を確認してください。 ## 基本的な例[​](#basic-example "この見出しへの直接リンク") 以下は、FSDを実装したシンプルなプロジェクトです。 * `📁 app` * `📁 pages` * `📁 shared` これらのトップレベルのフォルダーは*レイヤー*と呼ばれます。詳しく見てみましょう。 * `📂 app` * `📁 routes` * `📁 analytics` * `📂 pages` * `📁 home` * `📂 article-reader` * `📁 ui` * `📁 api` * `📁 settings` * `📂 shared` * `📁 ui` * `📁 api` `📂 pages`内のフォルダーは*スライス*と呼ばれます。スライスはドメイン(この場合はページ)ごとにレイヤーを分割します。 `📂 app`、`📂 shared`、および`📂 pages/article-reader`内のフォルダーは*セグメント*と呼ばれ、スライス(またはレイヤー)を技術的な目的に応じて分割します。 ## 概念[​](#concepts "この見出しへの直接リンク") レイヤー、スライス、セグメントは、以下の図に示されるように階層を形成します。 ![FSDの概念の階層、以下に説明](/documentation/ja/assets/images/visual_schema-e826067f573946613dcdc76e3f585082.jpg) 上の図には、左から右に「レイヤー」、「スライス」、「セグメント」とラベル付けされた3つの列があります。 「レイヤー」列には、上から下に「app」、「processes」、「pages」、「widgets」、「features」、「entities」、「shared」とラベル付けされた7つの区分があります。「processes」区分は取り消し線が引かれています。「entities」区分は2番目の列「スライス」と接続されていて、2番目の列が「entities」の内容であることを示しています。 「スライス」列には、上から下に「user」、「post」、「comment」とラベル付けされた3つの区分があります。「post」区分は「セグメント」列と同様に接続されていて、「post」の内容であることを示しています。 「セグメント」列には、上から下に「ui」、「model」、「api」とラベル付けされた3つの区分があります。 ### レイヤー[​](#layers "この見出しへの直接リンク") レイヤーはすべてのFSDプロジェクトで標準化されています。すべてのレイヤーを使用する必要はありませんが、ネーミングは重要です。現在、7つのレイヤーが存在しています(上から下へ)。 1. App*(アップ) — アプリケーションの起動に必要なすべてのもの(ルーティング、エントリーポイント、グローバルスタイル、プロバイダーなど) 2. Processes(プロセス、非推奨) — 複雑なページ間のシナリオ 3. Pages(ページ) — ページ全体、またはネストされたルーティングの場合、ページの大部分 4. Widgets(ウィジェット) — 大きな自己完結型の機能部分、またはインターフェースの大部分。通常はユーザーシナリオ全体を実装する 5. Features(フィーチャー) — プロダクト機能の再利用可能な実装、つまりユーザーにビジネス価値をもたらすアクション 6. Entities(エンティティ) — プロジェクトが扱うビジネスエンティティ、例えば`user`や`product` 7. Shared*(シェアード) — 再利用可能なコード。特にプロジェクト/ビジネスの詳細から切り離されたもの ** — App層とShared層のレイヤーは他のレイヤーとは異なり、スライスを持たず、直接セグメントで構成されています。* レイヤーの特徴は、レイヤーのモジュールは、下層のレイヤーモジュールのみを知ることができ、その結果、レイヤーが下層のレイヤーからのみモジュールをインポートできることです。 ### スライス[​](#slices "この見出しへの直接リンク") 次にスライスがあり、レイヤーをドメインごとに分割します。スライスの名前は自由に付けることができ、いくつでも作成できます。スライスは、意味的に関連するコードをグループ化することで、プロジェクト内のナビゲーションをしやすくします。 スライスは同じレイヤーの他のスライスを使用できないため、スライス内のコードの強い結合とスライス間の弱い結合が保証されます。 ### セグメント[​](#segments "この見出しへの直接リンク") スライス、およびApp層とShared層のレイヤーはセグメントで構成され、セグメントはその目的に応じてコードをグループ化します。セグメントの名前は標準で固定されていませんが、最も一般的な目的のためにいくつかの共通の名前があります。 * `ui` — 表示に関連するすべて: UIコンポーネント、日付フォーマッター、スタイルなど * `api` — バックエンドとのやり取り: リクエスト関数、データ型、マッパー * `model` — データモデル: バリデーションスキーマ、インターフェース、ストレージ、ビジネスロジック * `lib` — 他のモジュールが必要とするライブラリコード * `config` — 設定ファイルとフィーチャーフラグ 通常、これらのセグメントはほとんどのレイヤーに十分であるため、独自のセグメントはShared層やApp層でのみ作成されることが多いです。しかし、これは厳格なルールではありません。 ## 利点[​](#advantages "この見出しへの直接リンク") * **一貫性**
構造が標準化されているため、プロジェクトがより一貫性を持ち、新しいメンバーのチームへの参加が容易になります。 * **変更とリファクタリングへの耐性**
レイヤーのモジュールは、同じレイヤーや上層レイヤーの他のモジュールを使用できないため、アプリケーションの他の部分に予期しない影響を与えることなく、分離された変更を加えることができます。 * **ロジックの再利用制御**
レベルに応じて、コードを非常に再利用可能にすることも、非常にローカルにすることもできます。
これにより、**DRY**原則と実用性のバランスが保たれます。 * **ビジネスとユーザーのニーズに焦点を当てる**
アプリケーションはビジネスドメインに分割され、命名にはビジネス用語の使用が奨励されるため、プロジェクトの他の無関係な部分に完全に精通することなく、プロダクトで有用な作業を行うことができます。 ## 段階的な導入[​](#incremental-adoption "この見出しへの直接リンク") 既存のコードベースをFSDに移行したい場合は、以下の戦略をお勧めします。私たち自身の移行経験から、この方法は非常に効果的であることが分かりました。 1. App層とShared層のレイヤーを徐々に形成し、基盤を作る。 2. 既存のすべてのインターフェースコードをウィジェットとページに分散させる。FSDのルールに違反する依存関係があっても良い。 3. インポートのルール違反を徐々に修正しながら、エンティティやフィーチャーを抽出する。 リファクタリング中に新しい大きなエンティティを追加することや、部分的なリファクタリングは避けることをお勧めします。 ## 次のステップ[​](#next-steps "この見出しへの直接リンク") * **FSDの考え方を理解したい?** [チュートリアル](/documentation/ja/docs/get-started/tutorial.md)を読んでください。 * **例を見て学びたい?** [実装例セクション](/documentation/ja/examples.md)にたくさんあります。 * **質問がある?** [Discordチャンネル](https://discord.com/invite/S8MzWTUsmp)にアクセスして、コミュニティに質問してください。 --- # チュートリアル ## 第1章 紙の上で[​](#第1章-紙の上で "この見出しへの直接リンク") このガイドでは、Real World Appとしても知られるアプリケーションを見ていきます。Conduitは、[Medium](https://medium.com/)の簡略版であり、ブログ記事を読み書きし、他の人の記事にコメントすることができます。 ![Conduitのホームページ](/documentation/ja/assets/images/realworld-feed-anonymous-8cbba45f488931979f6c8da8968ad685.jpg) これはかなり小さなアプリケーションなので、過度に分解することなく開発を進めます。おそらく、アプリケーション全体は3つの層に収まります: **App層**、**Pages層**、**Shared層**。もしそうでなければ、進行に応じて追加の層を導入しましょう。準備はいいですか? ### ページの列挙から始める[​](#ページの列挙から始める "この見出しへの直接リンク") 上のスクリーンショットを見てみると、少なくとも次のページがあると推測できます。 * ホーム(記事のフィード) * ログインと登録 * 記事の閲覧 * 記事の編集 * ユーザープロフィールの閲覧 * プロフィールの編集(設定) これらの各ページは、*Pages*層の個別*スライス*になります。概要のセクションから思い出してください。スライスは単に層内のフォルダーであり、層は事前に定義された名前のフォルダーだけです。例えば、`pages`のようです。 したがって、私たちのPagesフォルダーは次のようになります。 ``` 📂 pages/ 📁 feed/ (フィード) 📁 sign-in/ (ログイン/登録) 📁 article-read/ (記事の閲覧) 📁 article-edit/ (記事の編集) 📁 profile/ (プロフィール) 📁 settings/ (設定) ``` Feature-Sliced Designの特徴は、ページが互いに依存できないことです。つまり、1つのページが他のページのコードをインポートすることはできません。これは**層のインポートルール**によって禁じられています。 *スライス内のモジュール(ファイル)は、下層にあるスライスのみをインポートできる。* この場合、ページはスライスであるため、そのページ内のモジュール(ファイル)は、他のページではなく、下層からのみコードをインポートできます。 ### フィードを詳しく見てみると[​](#フィードを詳しく見てみると "この見出しへの直接リンク") ![匿名訪問者の視点](/documentation/ja/assets/images/realworld-feed-anonymous-8cbba45f488931979f6c8da8968ad685.jpg) *匿名訪問者の視点* ![認証されたユーザーの視点](/documentation/ja/assets/images/realworld-feed-authenticated-15427d9ff7baae009b47b501bee6c059.jpg) *認証されたユーザーの視点* フィードページには3つの動的領域があります。 1. 認証状態を示すログインリンク 2. フィードをフィルタリングするタグ一覧 3. 1つ、または2つのフィード記事。各記事にはいいねボタンがある ログインリンクは、すべてのページで共通のヘッダーの一部であるため、一旦保留にしましょう。 #### タグ一覧[​](#タグ一覧 "この見出しへの直接リンク") タグ一覧を作成するには、すべての利用可能なタグを取得し、各タグをチップ([chip](https://m3.material.io/components/chips/overview))として表示し、選択されたタグをクライアント側のストレージに保存する必要があります。これらの操作は、「APIとのインタラクション」、「ユーザーインターフェース」、「データストレージ」のカテゴリに関連しています。Feature-Sliced Designでは、コードは目的に応じて*セグメント*に分けられます。セグメントはスライス内のフォルダーであり、目的を説明する任意の名前を持つことができます。いくつかの目的は非常に一般的であるため、いくつかの一般的な名前があります。 * 📂 `api/` バックエンドとのインタラクション * 📂 `ui/` 表示と外観を担当するコード * 📂 `model/` データとビジネスロジックのストレージ * 📂 `config/` フィーチャーフラグ、環境変数、その他の設定形式 タグを取得するコードは`api`に、タグコンポーネントは`ui`に、ストレージとのインタラクションは`model`に配置します。 #### 記事[​](#記事 "この見出しへの直接リンク") 同じ論理に従って、記事のフィードを同じ3つのセグメントに分けることができます。 * 📂 `api/`: ページごとの記事一覧を取得したり、いいねを残したりする * 📂 `ui/`: * タグを選択したときに追加のタブを表示できるタブ一覧 * 個別の記事 * ページネーション * 📂 `model/`: クライアントのストレージに保存された読み込まれた投稿と現在のページ(必要に応じて) ### 共通コードの再利用[​](#共通コードの再利用 "この見出しへの直接リンク") アプリケーションのページは通常、目的によって非常に異なりますが、全体で共通するものもあります。例えば、デザイン言語に対応するUIキットや、すべてが特定の認証メソッドを介してREST APIを通じて行われるというバックエンドにおける取り決めです。スライスは隔離されている必要があるため、コードの再利用は下層の**Shared層**を介して行われます。 Shared層は他の層とは異なり、スライスではなくセグメントを含むため、Shared層はレイヤーとスライスのハイブリッドです。 通常、Shared層内のコードは事前に作成されず、開発の過程で抽出されます。なぜなら、どの部分のコードが実際に再利用されるかが開発中に明らかになるからです。それでも、Shared層にどんなコードを保持するかを念頭に置いておくことは重要です。 * 📂 `ui/` — ビジネスロジックなしのUIキット。例えば、ボタン、ダイアログ、フォームフィールド。 * 📂 `api/` — バックエンドへのリクエスト用の便利なラッパー(例えば、ウェブの場合は、`fetch()`のラッパー) * 📂 `config/` — 環境変数の処理 * 📂 `i18n/` — 多言語対応の設定 * 📂 `router/` — ルーティングのプリミティブと定数 これらはShared層内のセグメントの例に過ぎません。これらのいずれかを省略したり、自分自身のセグメントを作成したりできます。新しいセグメントを作成する際に覚えておくべき唯一のことは、セグメントの名前は内容の**本質(何)**ではなく、**目的(なぜ)**を説明するものでなければなりません。`components`、`hooks`、`modals`のような名前は使用しない方が良いです。なぜなら、これらはファイルが本質的に何を含んでいるかを説明するものであり、コードが書かれた目的を説明するものではないからです。このようなネーミングの結果、チームは必要なものを見つけるためにフォルダーを掘り下げなければならず、さらに無関係なコードが隣接しているため、リファクタリング時にアプリケーションの大部分に影響を与え、レビューやテストが難しくなってしまいます。 ### 公開APIを定義する[​](#公開apiを定義する "この見出しへの直接リンク") Feature-Sliced Designの文脈において、*公開API*という用語は、スライス、またはセグメントが、プロジェクト内の他のモジュールがインポートできるものを宣言することを意味します。例えば、JavaScriptでは、他のファイルからオブジェクトを再エクスポートする`index.js`ファイルがこれに該当します。これにより、外部との契約(つまり、公開API)が変更されない限り、スライス内でのリファクタリングを自由にできます。 Shared層にはスライスがないため、通常、セグメントレベルで公開API(インデックス)を定義する方が便利です。そうすることで、Shared層からのインポートは自然に目的に応じて整理されます。他のレイヤーにはスライスがあるため、通常は1つのインデックスをスライスに定義し、スライス自身が内部のセグメントのセットを制御する方が実用的です。なぜなら、他のレイヤーは通常、エクスポートがはるかに少なく、リファクタリングが頻繁に行われるからです。 私たちのスライス/セグメントは次のようになるでしょう。 ``` 📂 pages/ 📂 feed/ 📄 index 📂 sign-in/ 📄 index 📂 article-read/ 📄 index 📁 … 📂 shared/ 📂 ui/ 📄 index 📂 api/ 📄 index 📁 … ``` `pages/feed`や`shared/ui`のようなフォルダー内にあるものは、これらのフォルダーにのみ知られており、これらのフォルダーの内容に関する保証はありません。 ### 大きな再利用可能なUIブロック[​](#大きな再利用可能なuiブロック "この見出しへの直接リンク") 以前、再利用可能なアプリケーションのヘッダーのところに戻りますが、各ページでヘッダーを再構築するのは非効率的なので、再利用します。再利用するコードには、すでにShared層がありますが、Shared層内の大きなUIブロックには注意が必要です。Shared層は上層のレイヤーについて何も知らないべきです。 Shared層とPages層の間には、Entities層、Features層、Widgets層の3つの他のレイヤーがあります。他のプロジェクトでは、これらのレイヤーに大きな再利用可能なブロックで使用したいものがあるかもしれません。その場合、そのブロックをShared層に置くことはできません。なぜなら、上層からインポートしなければならず、それは禁止されているからです。ここでWidgets層が役立ちます。これはShared層、Entities層、Features層の上に位置しているため、すべてを使用できます。 私たちの場合、ヘッダーは非常にシンプルです。静的なロゴと上部ナビゲーションしかありません。ナビゲーションはAPIに現在のユーザーが認証されているかどうかを尋ねる必要がありますが、これは`api`セグメントからの単純なインポートで解決できます。したがって、ヘッダーはShared層に残します。 ### フォームページに着目[​](#フォームページに着目 "この見出しへの直接リンク") 記事を読むだけでなく、編集することもできるページも見てみましょう。例えば、記事編集者のページです。 ![Conduitの記事編集者](/documentation/ja/assets/images/realworld-editor-authenticated-10de4d01479270886859e08592045b1e.jpg) 見た目は単純ですが、私たちがまだ調べていないアプリケーション開発のいくつかの側面を含んでいます。フォームのバリデーション、エラー状態、データの永続的な保存のようなものです。 このページを作成するには、Shared層からいくつかのフィールドとボタンを取り、それらをこのページの`ui`セグメントにあるフォームにまとめます。次に、`api`セグメントで、バックエンドに記事を作成するための変更リクエストを定義します。 リクエストを送信する前にリクエストをバリデーションするために、バリデーションスキーマが必要です。バリデーションスキーマはデータモデルであるため、`model`セグメントに入れるのがちょうど良いです。そこでエラーメッセージを生成し、`ui`セグメントの別のコンポーネントを使用してエラーメッセージを表示します。 UXを向上させるために、ブラウザを閉じたときに偶発的なデータ損失を防ぐために、入力データを永続的に保存することもできます。これも`model`セグメントに適しています。 ### まとめ[​](#まとめ "この見出しへの直接リンク") いくつかのページに着目し、アプリケーションの基本的な構造を決めることができました。 1. Shared層 1. `ui` には再利用可能なUIキットが含まれる 2. `api` にはバックエンドとのインタラクションのためのプリミティブが含まれる 3. 残りはコードを書く過程で整理する 2. Pages層 — 各ページに対して個別のスライスを作成 1. `ui` にはページ自体とその構成要素が含まれる 2. `api` には`shared/api`を使用するデータ取得のためのより専用的な関数が含まれる 3. `model` には表示するデータのクライアントストレージなどが含まれる これでこのアプリケーションを作りましょう! ## 第2章 コードの中で[​](#第2章-コードの中で "この見出しへの直接リンク") 計画ができたので、実現していきましょう。Reactと[Remix](https://remix.run/)を使用します。 このプロジェクトにはすでにテンプレートが用意されているので、GitHubからクローンして作成を始めてください。 依存関係を`npm install`でインストールし、`npm run dev`でサーバーを起動します。[http://localhost:3000](http://localhost:3000/)を開くと、空のアプリケーションが表示されます。 ### ページごとに整理する[​](#ページごとに整理する "この見出しへの直接リンク") すべてのページのために空のコンポーネントを作成することから始めましょう。ターミナルで次のコマンドを実行します。 ``` npx fsd pages feed sign-in article-read article-edit profile settings --segments ui ``` これにより、`pages/feed/ui/`のようなフォルダーと、各ページのインデックスファイル`pages/feed/index.ts`が作成されます。 ### フィードページを接続する[​](#フィードページを接続する "この見出しへの直接リンク") アプリケーションのルート(`/`)をフィードページに接続しましょう。`pages/feed/ui`に`FeedPage.tsx`コンポーネントを作成し、次の内容を入れます。 pages/feed/ui/FeedPage.tsx ``` export function FeedPage() { return (

conduit

知識を共有する場

); } ``` 次に、このコンポーネントをフィードページの公開APIに再エクスポートします。 pages/feed/index.ts ``` export { FeedPage } from "./ui/FeedPage"; ``` 次に、ルートに接続します。Remixでは、ルーティングはファイルに基づいていて、ルートファイルは`app/routes`フォルダーにあります。これはFeature-Sliced Designとよく組み合っています。 `app/routes/_index.tsx`で`FeedPage`コンポーネントを使用します。 app/routes/\_index.tsx ``` import type { MetaFunction } from "@remix-run/node"; import { FeedPage } from "pages/feed"; export const meta: MetaFunction = () => { return [{ title: "Conduit" }]; }; export default FeedPage; ``` これで、devサーバーを起動し、アプリケーションを開くと、Conduitのバナーが表示されるはずです! ![Conduitのバナー](/documentation/ja/assets/images/conduit-banner-a20e38edcd109ee21a8b1426d93a66b3.jpg) ### APIクライアント[​](#apiクライアント "この見出しへの直接リンク") RealWorldのバックエンドと通信するために、Shared層内に便利なAPIクライアントを作成しましょう。クライアント用の`api`セグメントと、バックエンドの基本URLなどの変数用の`config`セグメントを作成します。 ``` npx fsd shared --segments api config ``` 次に、`shared/config/backend.ts`を作成します。 shared/config/backend.ts ``` export { mockBackendUrl as backendBaseUrl } from "mocks/handlers"; ``` shared/config/index.ts ``` export { backendBaseUrl } from "./backend"; ``` RealWorldプロジェクトは[OpenAPI仕様](https://github.com/gothinkster/realworld/blob/main/api/openapi.yml)を提供しているため、APIクライアントの型を自動的に生成できます。私たちは[`openapi-fetch`パッケージ](https://openapi-ts.pages.dev/openapi-fetch/)を使用します。このパッケージにはTypeScriptの型を自動生成するツールも含まれています。 次のコマンドを実行して、APIの最新の型を生成しましょう。 ``` npm run generate-api-types ``` その結果、`shared/api/v1.d.ts`ファイルが作成されます。このファイルを使用して、`shared/api/client.ts`で型付きAPIクライアントを作成します。 shared/api/client.ts ``` import createClient from "openapi-fetch"; import { backendBaseUrl } from "shared/config"; import type { paths } from "./v1"; export const { GET, POST, PUT, DELETE } = createClient({ baseUrl: backendBaseUrl }); ``` shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; ``` ### フィード内の実データ[​](#フィード内の実データ "この見出しへの直接リンク") これで、バックエンドから記事を取得し、フィードに追加できます。まず、記事プレビューコンポーネントを実装しましょう。 `pages/feed/ui/ArticlePreview.tsx`を作成し、次の内容を記述します。 pages/feed/ui/ArticlePreview\.tsx ``` export function ArticlePreview({ article }) { /* TODO */ } ``` 私たちはTypeScriptを使っているので、型付きのArticleオブジェクトを持つと良いでしょう。生成された`v1.d.ts`を調べると、Articleオブジェクトは`components["schemas"]["Article"]`を介して利用可能であることがわかります。これでShared層内にデータモデルを持つファイルを作成し、モデルをエクスポートしましょう。 shared/api/models.ts ``` import type { components } from "./v1"; export type Article = components["schemas"]["Article"]; ``` shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; export type { Article } from "./models"; ``` これで、記事プレビューコンポーネントに戻って、データでマークアップを埋めることができます。次の内容をコンポーネントに追加します。 pages/feed/ui/ArticlePreview\.tsx ``` import { Link } from "@remix-run/react"; import type { Article } from "shared/api"; interface ArticlePreviewProps { article: Article; } export function ArticlePreview({ article }: ArticlePreviewProps) { return (
{article.author.username} {new Date(article.createdAt).toLocaleDateString(undefined, { dateStyle: "long", })}

{article.title}

{article.description}

続きを読む...
    {article.tagList.map((tag) => (
  • {tag}
  • ))}
); } ``` 「いいね」ボタンはまだ機能していません。それは記事の読み取りページに移動して、「いいね」機能を実装するときに修正します。 これで、記事を取得して、たくさんのプレビューカードを表示できます。Remixでは、データの取得は*ローダー*を使用して行われます。ローダーは、ページに必要なデータを収集するサーバー関数です。ローダーはページの代わりにAPIとやり取りをするため、`api`セグメントに配置します。 pages/feed/api/loader.ts ``` import { json } from "@remix-run/node"; import { GET } from "shared/api"; export const loader = async () => { const { data: articles, error, response } = await GET("/articles"); if (error !== undefined) { throw json(error, { status: response.status }); } return json({ articles }); }; ``` これをページに接続するには、ルートファイルから`loader`としてエクスポートする必要があります。 pages/feed/index.ts ``` export { FeedPage } from "./ui/FeedPage"; export { loader } from "./api/loader"; ``` app/routes/\_index.tsx ``` import type { MetaFunction } from "@remix-run/node"; import { FeedPage } from "pages/feed"; export { loader } from "pages/feed"; export const meta: MetaFunction = () => { return [{ title: "Conduit" }]; }; export default FeedPage; ``` 最後のステップは、これらのカードをフィードに表示することです。`FeedPage`を次のコードで更新します。 pages/feed/ui/FeedPage.tsx ``` import { useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; export function FeedPage() { const { articles } = useLoaderData(); return (

conduit

知識を共有する場

{articles.articles.map((article) => ( ))}
); } ``` ### タグによるフィルタリング[​](#タグによるフィルタリング "この見出しへの直接リンク") タグに関しては、バックエンドから取得し、ユーザーが選択したタグを記憶する必要があります。私たちはバックエンドからの取得方法はすでに知っています。これはローダー関数からの別のリクエストです。すでにインストールされている`remix-utils`パッケージの便利な`promiseHash`関数を使用します。 `pages/feed/api/loader.ts`のローダーを次のコードで更新します。 pages/feed/api/loader.ts ``` import { json } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async () => { return json( await promiseHash({ articles: throwAnyErrors(GET("/articles")), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` エラー処理を共通の`throwAnyErrors`関数に移したことに気付いたでしょうか。それはかなり使えそうに見えるので、後で再利用するかもしれません。 タグ一覧をインタラクティブにする必要があります。タグをクリックすると、そのタグが選択されるようにします。Remixの伝統に従い、選択されたタグのストレージとしてURLのクエリパラメータを使用します。ブラウザにストレージを任せ、私たちはより重要なことに集中しましょう。 `pages/feed/ui/FeedPage.tsx`を次のコードで更新します。 pages/feed/ui/FeedPage.tsx ``` import { Form, useLoaderData } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import type { loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; export function FeedPage() { const { articles, tags } = useLoaderData(); return (

conduit

知識を共有する場

{articles.articles.map((article) => ( ))}

人気のタグ

{tags.tags.map((tag) => ( ))}
); } ``` 次に、タグの検索パラメータをローダーで使用する必要があります。`pages/feed/api/loader.ts`の`loader`関数を次のように変更します。 pages/feed/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; return json( await promiseHash({ articles: throwAnyErrors( GET("/articles", { params: { query: { tag: selectedTag } } }), ), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` 以上です。最終的に`model`セグメントは必要ありませんでした。Remixはすごいですよね。 ### ページネーション[​](#ページネーション "この見出しへの直接リンク") 同様に、ページネーションを実装できます。自分で実装してみても、以下のコードをコピーしても良いです。 pages/feed/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } /** 1ページあたりの記事の数。 */ export const LIMIT = 20; export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; const page = parseInt(url.searchParams.get("page") ?? "", 10); return json( await promiseHash({ articles: throwAnyErrors( GET("/articles", { params: { query: { tag: selectedTag, limit: LIMIT, offset: !Number.isNaN(page) ? page * LIMIT : undefined, }, }, }), ), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` pages/feed/ui/FeedPage.tsx ``` import { Form, useLoaderData, useSearchParams } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import { LIMIT, type loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; export function FeedPage() { const [searchParams] = useSearchParams(); const { articles, tags } = useLoaderData(); const pageAmount = Math.ceil(articles.articlesCount / LIMIT); const currentPage = parseInt(searchParams.get("page") ?? "1", 10); return (

conduit

知識を共有する場

{articles.articles.map((article) => ( ))}
    {Array(pageAmount) .fill(null) .map((_, index) => index + 1 === currentPage ? (
  • {index + 1}
  • ) : (
  • ), )}

人気のタグ

{tags.tags.map((tag) => ( ))}
); } ``` よし、これも実現しました。タグ一覧も同様に実装できますが、認証を実装するまで待ちましょう。ところで、認証についてですが! ### 認証[​](#認証 "この見出しへの直接リンク") 認証には、ログイン用のページと登録用のページの2つがあります。これらは主に非常に似ているため、必要に応じてコードを再利用できるように、1つの`sign-in`セグメントに保持するのが理にかなっています。 `pages/sign-in`の`ui`セグメントに`RegisterPage.tsx`を作成し、次の内容を配置します。 pages/sign-in/ui/RegisterPage.tsx ``` import { Form, Link, useActionData } from "@remix-run/react"; import type { register } from "../api/register"; export function RegisterPage() { const registerData = useActionData(); return (

登録

アカウントをお持ちですか?

{registerData?.error && (
    {registerData.error.errors.body.map((error) => (
  • {error}
  • ))}
)}
); } ``` これからは壊れたインポートを修正する必要があります。インポートが新しいセグメントにアクセスしているため、次のコマンドでそのセグメントを作成しましょう。 ``` npx fsd pages sign-in -s api ``` ただし、登録のバックエンド部分を実装する前に、Remixのセッション処理のためのインフラコードが必要です。これは他のページでも必要になる可能性があるため、Shared層に配置します。 次のコードを`shared/api/auth.server.ts`に配置しましょう。このコードはRemixに特有のものであり、すべてが理解できなくても心配しないでください。単にコピーして貼り付けてください。 shared/api/auth.server.ts ``` import { createCookieSessionStorage, redirect } from "@remix-run/node"; import invariant from "tiny-invariant"; import type { User } from "./models"; invariant( process.env.SESSION_SECRET, "SESSION_SECRET must be set for authentication to work", ); const sessionStorage = createCookieSessionStorage<{ user: User; }>({ cookie: { name: "__session", httpOnly: true, path: "/", sameSite: "lax", secrets: [process.env.SESSION_SECRET], secure: process.env.NODE_ENV === "production", }, }); export async function createUserSession({ request, user, redirectTo, }: { request: Request; user: User; redirectTo: string; }) { const cookie = request.headers.get("Cookie"); const session = await sessionStorage.getSession(cookie); session.set("user", user); return redirect(redirectTo, { headers: { "Set-Cookie": await sessionStorage.commitSession(session, { maxAge: 60 * 60 * 24 * 7, // 7日間 }), }, }); } export async function getUserFromSession(request: Request) { const cookie = request.headers.get("Cookie"); const session = await sessionStorage.getSession(cookie); return session.get("user") ?? null; } export async function requireUser(request: Request) { const user = await getUserFromSession(request); if (user === null) { throw redirect("/login"); } return user; } ``` また、`models.ts`ファイルから`User`モデルをエクスポートしてください。 shared/api/models.ts ``` import type { components } from "./v1"; export type Article = components["schemas"]["Article"]; export type User = components["schemas"]["User"]; ``` このコードが動作する前に、`SESSION_SECRET`環境変数を設定する必要があります。プロジェクトのルートに`.env`ファイルを作成し、`SESSION_SECRET=`を記述してから、適当にランダムな文字列を記入します。次のようになります。 .env ``` SESSION_SECRET=これをコピーしないでください ``` 最後に、公開APIにいくつかのエクスポートを追加します。 shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; export type { Article } from "./models"; export { createUserSession, getUserFromSession, requireUser } from "./auth.server"; ``` これで、RealWorldのバックエンドと通信するコードを書くことができます。これを`pages/sign-in/api`に保存します。`register.ts`ファイルを作成して、中に次のコードを配置しましょう。 pages/sign-in/api/register.ts ``` import { json, type ActionFunctionArgs } from "@remix-run/node"; import { POST, createUserSession } from "shared/api"; export const register = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const username = formData.get("username")?.toString() ?? ""; const email = formData.get("email")?.toString() ?? ""; const password = formData.get("password")?.toString() ?? ""; const { data, error } = await POST("/users", { body: { user: { email, password, username } }, }); if (error) { return json({ error }, { status: 400 }); } else { return createUserSession({ request: request, user: data.user, redirectTo: "/", }); } }; ``` pages/sign-in/index.ts ``` export { RegisterPage } from './ui/RegisterPage'; export { register } from './api/register'; ``` ほぼ完成です!残りの部分は、`/register`ルートにアクションとページを接続することだけです。`app/routes`で`register.tsx`を作成します。 app/routes/register.tsx ``` import { RegisterPage, register } from "pages/sign-in"; export { register as action }; export default RegisterPage; ``` これで、にアクセスすると、ユーザーを作成できます!アプリケーションの残りの部分は、まだ反応しませんが、近々対処します。 同様に、ログインページを実装することもできます。自分で実装してみるか、下記のコードをコピペするか、次に進みましょう。 pages/sign-in/api/sign-in.ts ``` import { json, type ActionFunctionArgs } from "@remix-run/node"; import { POST, createUserSession } from "shared/api"; export const signIn = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const email = formData.get("email")?.toString() ?? ""; const password = formData.get("password")?.toString() ?? ""; const { data, error } = await POST("/users/login", { body: { user: { email, password } }, }); if (error) { return json({ error }, { status: 400 }); } else { return createUserSession({ request: request, user: data.user, redirectTo: "/", }); } }; ``` pages/sign-in/ui/SignInPage.tsx ``` import { Form, Link, useActionData } from "@remix-run/react"; import type { signIn } from "../api/sign-in"; export function SignInPage() { const signInData = useActionData(); return (

サインイン

アカウントが必要ですか?

{signInData?.error && (
    {signInData.error.errors.body.map((error) => (
  • {error}
  • ))}
)}
); } ``` pages/sign-in/index.ts ``` export { RegisterPage } from './ui/RegisterPage'; export { register } from './api/register'; export { SignInPage } from './ui/SignInPage'; export { signIn } from './api/sign-in'; ``` app/routes/login.tsx ``` import { SignInPage, signIn } from "pages/sign-in"; export { signIn as action }; export default SignInPage; ``` これで、ユーザーがこれらのページにアクセスできるようになりました。 ### ヘッダー[​](#ヘッダー "この見出しへの直接リンク") 前章で説明されたように、アプリケーションのヘッダーは通常Widgets層、またはShared層に配置されます。ヘッダーは非常にシンプルで、すべてのビジネスロジックを外部に保持できるので、Shared層に配置しましょう。ヘッダー用のフォルダーを作成します。 ``` npx fsd shared ui ``` 次に、`shared/ui/Header.tsx`を作成し、次の内容を配置します。 shared/ui/Header.tsx ``` import { useContext } from "react"; import { Link, useLocation } from "@remix-run/react"; import { CurrentUser } from "../api/currentUser"; export function Header() { const currentUser = useContext(CurrentUser); const { pathname } = useLocation(); return ( ); } ``` このコンポーネントを`shared/ui`からエクスポートします。 shared/ui/index.ts ``` export { Header } from "./Header"; ``` ヘッダーで`shared/api`にあるコンテキストを使っているので、それを作成しましょう。 shared/api/currentUser.ts ``` import { createContext } from "react"; import type { User } from "./models"; export const CurrentUser = createContext(null); ``` shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; export type { Article } from "./models"; export { createUserSession, getUserFromSession, requireUser } from "./auth.server"; export { CurrentUser } from "./currentUser"; ``` これで、ヘッダーをページに追加できます。すべてのページに表示されるように、ルートに追加し、アウトレット(ページがレンダリングされる場所)を`CurrentUser`のコンテキストプロバイダーで包みます。これにより、ヘッダーを含むアプリ全体が現在のユーザーオブジェクトにアクセスできるようになります。また、クッキーから現在のユーザーオブジェクトを取得するためのローダーも追加します。次のコードを`app/root.tsx`に追加しましょう。 app/root.tsx ``` import { cssBundleHref } from "@remix-run/css-bundle"; import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node"; import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData, } from "@remix-run/react"; import { Header } from "shared/ui"; import { getUserFromSession, CurrentUser } from "shared/api"; export const links: LinksFunction = () => [ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), ]; export const loader = ({ request }: LoaderFunctionArgs) => getUserFromSession(request); export default function App() { const user = useLoaderData(); return (
); } ``` 最終的に、ホームページは次のようになります。 ![ヘッダー、フィード、タグがあるConduitのフィードページ。タブはまだありません。](/documentation/ja/assets/images/realworld-feed-without-tabs-5da4c9072101ac20e82e2234bd3badbe.jpg) ヘッダー、フィード、タグがあるConduitのフィードページ。タブはまだない。 ### タブ[​](#タブ "この見出しへの直接リンク") これで認証状態を判断できるようになったので、タブと「いいね」ボタンをフィードページに実装しましょう。新しいフォームを作る必要がありますが、このページファイルはすでに大きすぎるので、これらのフォームを隣接するファイルに移動しましょう。`Tabs.tsx`、`PopularTags.tsx`、`Pagination.tsx`を作成し、次の内容を配置します。 pages/feed/ui/Tabs.tsx ``` import { useContext } from "react"; import { Form, useSearchParams } from "@remix-run/react"; import { CurrentUser } from "shared/api"; export function Tabs() { const [searchParams] = useSearchParams(); const currentUser = useContext(CurrentUser); return (
    {currentUser !== null && (
  • )}
  • {searchParams.has("tag") && (
  • {searchParams.get("tag")}
  • )}
); } ``` pages/feed/ui/PopularTags.tsx ``` import { Form, useLoaderData } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import type { loader } from "../api/loader"; export function PopularTags() { const { tags } = useLoaderData(); return (

人気のタグ

{tags.tags.map((tag) => ( ))}
); } ``` pages/feed/ui/Pagination.tsx ``` import { Form, useLoaderData, useSearchParams } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import { LIMIT, type loader } from "../api/loader"; export function Pagination() { const [searchParams] = useSearchParams(); const { articles } = useLoaderData(); const pageAmount = Math.ceil(articles.articlesCount / LIMIT); const currentPage = parseInt(searchParams.get("page") ?? "1", 10); return (
    {Array(pageAmount) .fill(null) .map((_, index) => index + 1 === currentPage ? (
  • {index + 1}
  • ) : (
  • ), )}
); } ``` これで、フィードページを大幅に簡素化できます。 pages/feed/ui/FeedPage.tsx ``` import { useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; import { Tabs } from "./Tabs"; import { PopularTags } from "./PopularTags"; import { Pagination } from "./Pagination"; export function FeedPage() { const { articles } = useLoaderData(); return (

conduit

知識を共有する場

{articles.articles.map((article) => ( ))}
); } ``` ローダー関数にも新しいタブを考慮する必要があります。 pages/feed/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET, requireUser } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { /* そのまま */ } /** 1ページあたりの記事数。 */ export const LIMIT = 20; export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; const page = parseInt(url.searchParams.get("page") ?? "", 10); if (url.searchParams.get("source") === "my-feed") { const userSession = await requireUser(request); return json( await promiseHash({ articles: throwAnyErrors( GET("/articles/feed", { params: { query: { limit: LIMIT, offset: !Number.isNaN(page) ? page * LIMIT : undefined, }, }, headers: { Authorization: `Token ${userSession.token}` }, }), ), tags: throwAnyErrors(GET("/tags")), }), ); } return json( await promiseHash({ articles: throwAnyErrors( GET("/articles", { params: { query: { tag: selectedTag, limit: LIMIT, offset: !Number.isNaN(page) ? page * LIMIT : undefined, }, }, }), ), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` フィードページを一旦置いておく前に、投稿へのいいねを処理するコードを追加しましょう。`ArticlePreview.tsx`を次のように変更します。 pages/feed/ui/ArticlePreview\.tsx ``` import { Form, Link } from "@remix-run/react"; import type { Article } from "shared/api"; interface ArticlePreviewProps { article: Article; } export function ArticlePreview({ article }: ArticlePreviewProps) { return (
{article.author.username} {new Date(article.createdAt).toLocaleDateString(undefined, { dateStyle: "long", })}

{article.title}

{article.description}

もっと読む...
    {article.tagList.map((tag) => (
  • {tag}
  • ))}
); } ``` このコードは、`/article/:slug`にPOSTリクエストを送信し、`_action=favorite`を使用して記事をお気に入りにします。今は機能していませんが、記事リーダーの作成を始めると、これも実装します。 これで、フィードの作成が完了しました!やったね! ### 記事リーダー[​](#記事リーダー "この見出しへの直接リンク") まず、データが必要です。ローダーを作成しましょう。 ``` npx fsd pages article-read -s api ``` pages/article-read/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import invariant from "tiny-invariant"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET, getUserFromSession } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async ({ request, params }: LoaderFunctionArgs) => { invariant(params.slug, "スラッグパラメータが必要です"); const currentUser = await getUserFromSession(request); const authorization = currentUser ? { Authorization: `Token ${currentUser.token}` } : undefined; return json( await promiseHash({ article: throwAnyErrors( GET("/articles/{slug}", { params: { path: { slug: params.slug }, }, headers: authorization, }), ), comments: throwAnyErrors( GET("/articles/{slug}/comments", { params: { path: { slug: params.slug }, }, headers: authorization, }), ), }), ); }; ``` pages/article-read/index.ts ``` export { loader } from "./api/loader"; ``` これで、`/article/:slug`ルートに接続できます。`article.$slug.tsx`というルートファイルを作成します。 app/routes/article.$slug.tsx ``` export { loader } from "pages/article-read"; ``` ページ自体は、記事のタイトルとアクション、記事の本文、コメントセクションの3つの主要なブロックで構成されています。下記はページのマークアップで、特に興味深いものはありません。 pages/article-read/ui/ArticleReadPage.tsx ``` import { useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { ArticleMeta } from "./ArticleMeta"; import { Comments } from "./Comments"; export function ArticleReadPage() { const { article } = useLoaderData(); return (

{article.article.title}

{article.article.body}

    {article.article.tagList.map((tag) => (
  • {tag}
  • ))}

); } ``` 興味深いのは`ArticleMeta`と`Comments`です。これらは、記事を「いいね」したり、コメントを残したりするための操作を含んでいます。これらが機能するためには、まずバックエンド部分を実装する必要があります。このページの`api`セグメントに`action.ts`ファイルを作成します。 pages/article-read/api/action.ts ``` import { redirect, type ActionFunctionArgs } from "@remix-run/node"; import { namedAction } from "remix-utils/named-action"; import { redirectBack } from "remix-utils/redirect-back"; import invariant from "tiny-invariant"; import { DELETE, POST, requireUser } from "shared/api"; export const action = async ({ request, params }: ActionFunctionArgs) => { const currentUser = await requireUser(request); const authorization = { Authorization: `Token ${currentUser.token}` }; const formData = await request.formData(); return namedAction(formData, { async delete() { invariant(params.slug, "スラッグパラメータが必要です"); await DELETE("/articles/{slug}", { params: { path: { slug: params.slug } }, headers: authorization, }); return redirect("/"); }, async favorite() { invariant(params.slug, "スラッグパラメータが必要です"); await POST("/articles/{slug}/favorite", { params: { path: { slug: params.slug } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async unfavorite() { invariant(params.slug, "スラッグパラメータが必要です"); await DELETE("/articles/{slug}/favorite", { params: { path: { slug: params.slug } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async createComment() { invariant(params.slug, "スラッグパラメータが必要です"); const comment = formData.get("comment"); invariant(typeof comment === "string", "コメントパラメータが必要です"); await POST("/articles/{slug}/comments", { params: { path: { slug: params.slug } }, headers: { ...authorization, "Content-Type": "application/json" }, body: { comment: { body: comment } }, }); return redirectBack(request, { fallback: "/" }); }, async deleteComment() { invariant(params.slug, "スラッグパラメータが必要です"); const commentId = formData.get("id"); invariant(typeof commentId === "string", "idパラメータが必要です"); const commentIdNumeric = parseInt(commentId, 10); invariant( !Number.isNaN(commentIdNumeric), "数値のidパラメータが必要です", ); await DELETE("/articles/{slug}/comments/{id}", { params: { path: { slug: params.slug, id: commentIdNumeric } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async followAuthor() { const authorUsername = formData.get("username"); invariant( typeof authorUsername === "string", "ユーザーネームパラメータが必要です", ); await POST("/profiles/{username}/follow", { params: { path: { username: authorUsername } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async unfollowAuthor() { const authorUsername = formData.get("username"); invariant( typeof authorUsername === "string", "ユーザーネームパラメータが必要です", ); await DELETE("/profiles/{username}/follow", { params: { path: { username: authorUsername } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, }); }; ``` これをスライスから再エクスポートし、ルートから再エクスポートします。ここにいる間に、ページ自体も接続しましょう。 pages/article-read/index.ts ``` export { ArticleReadPage } from "./ui/ArticleReadPage"; export { loader } from "./api/loader"; export { action } from "./api/action"; ``` app/routes/article.$slug.tsx ``` import { ArticleReadPage } from "pages/article-read"; export { loader, action } from "pages/article-read"; export default ArticleReadPage; ``` これで、記事リーダーの「いいね」ボタンはまだ実装されていないにも関わらず、フィードの「いいね」ボタンは機能し始めます!それは、フィードの「いいね」ボタンもそのルートにリクエストを送っているからです。何かを「いいね」してみてください! `ArticleMeta`と`Comments`は、単なるフォームです。以前にこれを行ったので、コードをコピペして先に進みましょう。 pages/article-read/ui/ArticleMeta.tsx ``` import { Form, Link, useLoaderData } from "@remix-run/react"; import { useContext } from "react"; import { CurrentUser } from "shared/api"; import type { loader } from "../api/loader"; export function ArticleMeta() { const currentUser = useContext(CurrentUser); const { article } = useLoaderData(); return (
{article.article.author.username} {article.article.createdAt}
{article.article.author.username == currentUser?.username ? ( <> 記事を編集    ) : ( <>    )}
); } ``` pages/article-read/ui/Comments.tsx ``` import { useContext } from "react"; import { Form, Link, useLoaderData } from "@remix-run/react"; import { CurrentUser } from "shared/api"; import type { loader } from "../api/loader"; export function Comments() { const { comments } = useLoaderData(); const currentUser = useContext(CurrentUser); return (
{currentUser !== null ? (
) : (

サインイン   または   登録   して記事にコメントを追加しましょう!

)} {comments.comments.map((comment) => (

{comment.body}

  {comment.author.username} {comment.createdAt} {comment.author.username === currentUser?.username && (
)}
))}
); } ``` これで、記事リーダーが完成しました!「著者をフォローする」ボタン、「いいね」ボタン、「コメントを残す」ボタンがすべて正常に機能するはずです。 ![記事リーダーの画像](/documentation/ja/assets/images/realworld-article-reader-6a420e4f2afe139d2bdd54d62974f0b9.jpg) 記事リーダーの画像 ### 記事編集[​](#記事編集 "この見出しへの直接リンク") これは、このガイドで最後に取り上げるページです。ここで最も興味深い部分は、フォームデータを検証する方法です。 `article-edit/ui/ArticleEditPage.tsx`ページ自体は、非常にシンプルで、追加のロジックは他の2つのコンポーネントに含まれます。 pages/article-edit/ui/ArticleEditPage.tsx ``` import { Form, useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { TagsInput } from "./TagsInput"; import { FormErrors } from "./FormErrors"; export function ArticleEditPage() { const article = useLoaderData(); return (
); } ``` このページは、存在する記事を取得し(新しい記事を作成する場合を除く)、対応するフォームフィールドを埋めます。これは以前に見たものです。着目すべき部分は`FormErrors`で、これは検証結果を取得し、ユーザーに表示します。 pages/article-edit/ui/FormErrors.tsx ``` import { useActionData } from "@remix-run/react"; import type { action } from "../api/action"; export function FormErrors() { const actionData = useActionData(); return actionData?.errors != null ? (
    {actionData.errors.map((error) => (
  • {error}
  • ))}
) : null; } ``` アクションが`errors`フィールドを返し、人間に理解できるエラーメッセージの配列を表示することを想定しています。アクションには後で移ります。 もう1つのコンポーネントはタグ入力フィールドです。これは、選択されたタグのプレビューできる通常の入力フィールドです。特に注目すべき点はありません。 pages/article-edit/ui/TagsInput.tsx ``` import { useEffect, useRef, useState } from "react"; export function TagsInput({ name, defaultValue, }: { name: string; defaultValue?: Array; }) { const [tagListState, setTagListState] = useState(defaultValue ?? []); function removeTag(tag: string): void { const newTagList = tagListState.filter((t) => t !== tag); setTagListState(newTagList); } const tagsInput = useRef(null); useEffect(() => { tagsInput.current && (tagsInput.current.value = tagListState.join(",")); }, [tagListState]); return ( <> setTagListState(e.target.value.split(",").filter(Boolean)) } />
{tagListState.map((tag) => ( [" ", "Enter"].includes(e.key) && removeTag(tag) } onClick={() => removeTag(tag)} >{" "} {tag} ))}
); } ``` 次に、API部分に移ります。ローダーはURLを確認し、記事へのリンクがある場合、既存の記事を編集していることを意味し、そのデータをロードする必要があります。そうでない場合は、何も返しません。このローダーを作成しましょう。 pages/article-edit/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { GET, requireUser } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async ({ params, request }: LoaderFunctionArgs) => { const currentUser = await requireUser(request); if (!params.slug) { return { article: null }; } return throwAnyErrors( GET("/articles/{slug}", { params: { path: { slug: params.slug } }, headers: { Authorization: `Token ${currentUser.token}` }, }), ); }; ``` アクションは新しいフィールドの値を受け取り、それらをデータスキーマに通し、すべてが正しければ、既存の記事を更新するか、新しい記事を作成することによって、バックエンドに変更を保存します。 pages/article-edit/api/action.ts ``` import { json, redirect, type ActionFunctionArgs } from "@remix-run/node"; import { POST, PUT, requireUser } from "shared/api"; import { parseAsArticle } from "../model/parseAsArticle"; export const action = async ({ request, params }: ActionFunctionArgs) => { try { const { body, description, title, tags } = parseAsArticle( await request.formData(), ); const tagList = tags?.split(",") ?? []; const currentUser = await requireUser(request); const payload = { body: { article: { title, description, body, tagList, }, }, headers: { Authorization: `Token ${currentUser.token}` }, }; const { data, error } = await (params.slug ? PUT("/articles/{slug}", { params: { path: { slug: params.slug } }, ...payload, }) : POST("/articles", payload)); if (error) { return json({ errors: error }, { status: 422 }); } return redirect(`/article/${data.article.slug ?? ""}`); } catch (errors) { return json({ errors }, { status: 400 }); } }; ``` 私たちのデータスキーマは、`FormData`を解析するので、最終的に処理するエラーメッセージを投げたりするのに役立ちます。この解析関数は次のようになります。 pages/article-edit/model/parseAsArticle.ts ``` export function parseAsArticle(data: FormData) { const errors = []; const title = data.get("title"); if (typeof title !== "string" || title === "") { errors.push("記事にタイトルを付けてください"); } const description = data.get("description"); if (typeof description !== "string" || description === "") { errors.push("この記事が何についてか説明してください"); } const body = data.get("body"); if (typeof body !== "string" || body === "") { errors.push("記事そのものを書いてください"); } const tags = data.get("tags"); if (typeof tags !== "string") { errors.push("タグは文字列である必要があります"); } if (errors.length > 0) { throw errors; } return { title, description, body, tags: data.get("tags") ?? "" } as { title: string; description: string; body: string; tags: string; }; } ``` 少し長く繰り返しが多いように見えるかもしれませんが、これはエラーメッセージを人間に理解しやすくするための代償です。Zodのようなスキーマを使用することもできますが、その場合、フロントエンドでエラーメッセージを表示する必要があります。このフォームはそのような複雑さには値しません。 最後のステップは、ページ、ローダー、アクションをルートに接続することです。私たちは作成と編集の両方をきれいにサポートしているので、`editor._index.tsx`と`editor.$slug.tsx`の両方から同じアクションをエクスポートできます。 pages/article-edit/index.ts ``` export { ArticleEditPage } from "./ui/ArticleEditPage"; export { loader } from "./api/loader"; export { action } from "./api/action"; ``` app/routes/editor.\_index.tsx, app/routes/editor.$slug.tsx (同じ内容) ``` import { ArticleEditPage } from "pages/article-edit"; export { loader, action } from "pages/article-edit"; export default ArticleEditPage; ``` これで完成です!ログインして新しい記事を作成してみてください。あるいは、フィールドに何も記入せず進み、バリデーションがどのように機能するかを検証してみてください。 ![記事編集者の画像](/documentation/ja/assets/images/realworld-article-editor-bc3ee45c96ae905fdbb54d6463d12723.jpg) 記事編集画像 プロフィールページや設定ページは、記事の読み取りや編集ページに非常に似ていて、読者のための宿題として残されています。 --- # Handling API Requests ## Shared API Requests[​](#shared-api-requests "この見出しへの直接リンク") Start by placing common API request logic in the `shared/api` directory. This makes it easy to reuse requests across your application and helps with faster prototyping. For many projects, this is all you'll need for API calls. A typical file structure would be: * 📂 shared * 📂 api * 📄 client.ts * 📄 index.ts * 📂 endpoints * 📄 login.ts The `client.ts` file centralizes your HTTP request setup. It wraps your chosen method (like `fetch()` or an `axios` instance) and handles common configurations, such as: * Backend base URL. * Default headers (e.g., for authentication). * Data serialization. Here are examples for `axios` and `fetch`: * Axios * Fetch shared/api/client.ts ``` // Example using axios import axios from 'axios'; export const client = axios.create({ baseURL: 'https://your-api-domain.com/api/', timeout: 5000, headers: { 'X-Custom-Header': 'my-custom-value' } }); ``` shared/api/client.ts ``` export const client = { async post(endpoint: string, body: any, options?: RequestInit) { const response = await fetch(`https://your-api-domain.com/api${endpoint}`, { method: 'POST', body: JSON.stringify(body), ...options, headers: { 'Content-Type': 'application/json', 'X-Custom-Header': 'my-custom-value', ...options?.headers, }, }); return response.json(); } // ... other methods like put, delete, etc. }; ``` Organize your individual API request functions in `shared/api/endpoints`, grouping them by the API endpoint. 注記 To keep examples focused, we omit form interaction and validation. For details on libraries like Zod or Valibot, refer to the [Type Validation and Schemas](/documentation/ja/docs/guides/examples/types.md#type-validation-schemas-and-zod) article. shared/api/endpoints/login.ts ``` import { client } from '../client'; export interface LoginCredentials { email: string; password: string; } export function login(credentials: LoginCredentials) { return client.post('/login', credentials); } ``` Use an `index.ts` file in `shared/api` to export your request functions. shared/api/index.ts ``` export { client } from './client'; // If you want to export the client itself export { login } from './endpoints/login'; export type { LoginCredentials } from './endpoints/login'; ``` ## Slice-Specific API Requests[​](#slice-specific-api-requests "この見出しへの直接リンク") If an API request is only used by a specific slice (like a single page or feature) and won't be reused, place it in the api segment of that slice. This keeps slice-specific logic neatly contained. * 📂 pages * 📂 login * 📄 index.ts * 📂 api * 📄 login.ts * 📂 ui * 📄 LoginPage.tsx pages/login/api/login.ts ``` import { client } from 'shared/api'; interface LoginCredentials { email: string; password: string; } export function login(credentials: LoginCredentials) { return client.post('/login', credentials); } ``` You don't need to export `login()` function in the page's public API, because it's unlikely that any other place in the app will need this request. 注記 Avoid placing API calls and response types in the `entities` layer prematurely. Backend responses may differ from what your frontend entities need. API logic in `shared/api` or a slice's `api` segment allows you to transform data appropriately, keeping entities focused on frontend concerns. ## Using Client Generators[​](#client-generators "この見出しへの直接リンク") If your backend has an OpenAPI specification, tools like [orval](https://orval.dev/) or [openapi-typescript](https://openapi-ts.dev/) can generate API types and request functions for you. Place the generated code in, for example, `shared/api/openapi`. Make sure to include `README.md` to document what those types are, and how to generate them. ## Integrating with Server State Libraries[​](#server-state-libraries "この見出しへの直接リンク") When using server state libraries like [TanStack Query (React Query)](https://tanstack.com/query/latest) or [Pinia Colada](https://pinia-colada.esm.dev/) you might need to share types or cache keys between slices. Use the `shared` layer for things like: * API data types * Cache keys * Common query/mutation options For more details on how to work with server state libraries, refer to [React Query article](/documentation/ja/docs/guides/tech/with-react-query.md) --- # 認証 一般的に、認証は以下のステップで構成されます。 1. ユーザーから資格情報を取得する 2. それをバックエンドに送信する 3. 認証されたリクエストを送信するためのトークンを保存する ## ユーザーの資格情報を取得する方法[​](#ユーザーの資格情報を取得する方法 "この見出しへの直接リンク") OAuthを通じて認証を行う場合は、OAuthプロバイダーのページへのリンクを持つログインページを作成し、[ステップ3](#how-to-store-the-token-for-authenticated-requests)に進むことができます。 ### ログイン用の別ページ[​](#ログイン用の別ページ "この見出しへの直接リンク") 通常、ウェブサイトにはユーザー名とパスワードを入力するためのログイン専用ページがあります。これらのページは非常にシンプルであるため、分解する必要はありません。さらに、ログインフォームと登録フォームは外見が非常に似ているため、同じページにグループ化することもできます。ログイン/登録ページ用のスライスをPages層に作成します。 * 📂 pages * 📂 login * 📂 ui * 📄 LoginPage.tsx * 📄 RegisterPage.tsx * 📄 index.ts * その他のページ… ここでは、2つのコンポーネントを作成し、インデックスで両方をエクスポートしました。これらのコンポーネントは、ユーザーが資格情報を入力するためのわかりやすい要素を含むフォームを持ちます。 ### ログイン用のダイアログボックス[​](#ログイン用のダイアログボックス "この見出しへの直接リンク") アプリケーションにどのページでも使用できるログイン用のダイアログボックスがある場合は、そのダイアログボックス用のウィジェットを作成できます。これにより、フォーム自体をあまり分解せずに、どのページでもこのダイアログボックスを再利用できます。 * 📂 widgets * 📂 login-dialog * 📂 ui * 📄 LoginDialog.tsx * 📄 index.ts * その他のウィジェット… このガイドの残りの部分は、ログインが別ページで行われる最初のアプローチに基づいていますが、同じ原則がダイアログボックス用のウィジェットにも適用されます。 ### クライアントバリデーション[​](#クライアントバリデーション "この見出しへの直接リンク") たまには、特に登録時に、クライアント側で検証を行い、ユーザーにエラーを迅速に通知することがあります。この場合、検証は、ログインページの`model`セグメントで行うことができます。スキーマ検証ライブラリ、例えば[Zod](https://zod.dev)をJS/TS用に使用し、このスキーマを`ui`セグメントに提供します。 pages/login/model/registration-schema.ts ``` import { z } from "zod"; export const registrationData = z.object({ email: z.string().email(), password: z.string().min(6), confirmPassword: z.string(), }).refine((data) => data.password === data.confirmPassword, { message: "パスワードが一致しません", path: ["confirmPassword"], }); ``` 次に、`ui`セグメントでこのスキーマを使用してユーザー入力を検証できます。 pages/login/ui/RegisterPage.tsx ``` import { registrationData } from "../model/registration-schema"; function validate(formData: FormData) { const data = Object.fromEntries(formData.entries()); try { registrationData.parse(data); } catch (error) { // TODO: ユーザーにエラーメッセージを表示 } } export function RegisterPage() { return (
validate(new FormData(e.target))}>
) } ``` ## 資格情報をバックエンドに送信する方法[​](#資格情報をバックエンドに送信する方法 "この見出しへの直接リンク") バックエンドのログインエンドポイントにリクエストを送信する関数を作成しましょう。この関数は、コンポーネントのコード内でミューテーションライブラリ(例えば、TanStack Query)を通じて直接呼び出すことも、状態管理ライブラリの副作用として呼び出すこともできます。 ### リクエスト関数をどこに置くか[​](#リクエスト関数をどこに置くか "この見出しへの直接リンク") この関数を置く場所は2つあります: `shared/api`またはページの`api`セグメントです。 #### `shared/api`に[​](#sharedapiに "この見出しへの直接リンク") このアプローチは、すべてのリクエスト関数を`shared/api`に配置し、エンドポイントごとにグループ化するのに適しています。この場合、ファイル構造は次のようになります。 * 📂 shared * 📂 api * 📂 endpoints * 📄 login.ts * その他のリクエスト関数… * 📄 client.ts * 📄 index.ts `📄 client.ts`ファイルには、リクエストを実行するためのプリミティブのラッパーが含まれています(例えば、`fetch()`)。このラッパーは、バックエンドのベースURLを知っており、必要なヘッダーを設定し、データをシリアライズします。 shared/api/endpoints/login.ts ``` import { POST } from "../client"; export function login({ email, password }: { email: string, password: string }) { return POST("/login", { email, password }); } ``` shared/api/index.ts ``` export { login } from "./endpoints/login"; ``` #### ページの`api`セグメントに[​](#ページのapiセグメントに "この見出しへの直接リンク") すべてのリクエストを1か所に保存していない場合は、ログインページの`api`セグメントにこのリクエスト関数を配置するのが適しているかもしれません。 * 📂 pages * 📂 login * 📂 api * 📄 login.ts * 📂 ui * 📄 LoginPage.tsx * 📄 index.ts * その他のページ… pages/login/api/login.ts ``` import { POST } from "shared/api"; export function login({ email, password }: { email: string, password: string }) { return POST("/login", { email, password }); } ``` この関数は、ページのインデックスから再エクスポートする必要はありません。なぜなら、恐らくこのページ内でのみ使用されるからです。 ### 2要素認証[​](#2要素認証 "この見出しへの直接リンク") アプリケーションが2要素認証(2FA)をサポートしている場合、ユーザーを一時的なパスワードを入力するための別のページにリダイレクトする必要があるかもしれません。通常、`POST /login`リクエストは、ユーザーに2FAが有効であることを示すフラグを持つユーザーオブジェクトを返します。このフラグが設定されている場合、ユーザーを2FAページにリダイレクトします。 このページはログインと非常に関連しているため、Pages層の同じ`login`スライスに配置することもできます。 また、上で作成した`login()`に似た別のリクエスト関数が必要になります。それらをShared層にまとめるか、ログインページの`api`セグメントに配置してください。 ## 認証されたリクエスト用のトークンを保存する方法[​](#how-to-store-the-token-for-authenticated-requests "この見出しへの直接リンク") 使用する認証スキームに関係なく、単純なログインとパスワード、OAuth、または2要素認証であっても、最終的にはトークンを取得します。以降のリクエストで自分を識別できるように、このトークンは保存する必要があります。 ウェブアプリケーションにおけるトークンの理想的な保存場所は**クッキー**です。クッキーはトークンの手動保存や処理を必要としません。したがって、クッキーの保存はフロントエンドアーキテクチャにほとんど労力を必要としません。フロントエンドフレームワークにサーバーサイドがある場合(例えば、[Remix](https://remix.run))、クッキーのサーバーインフラは`shared/api`に保存する必要があります。[「認証」チュートリアルセクション](/documentation/ja/docs/get-started/tutorial.md#authentication)には、Remixでの実装例があります。 ただし、時にはトークンをクッキーに保存することができない場合もあります。この場合、トークンを自分で保存しなければなりません。その際、トークンの有効期限が切れたときに更新するロジックを書く手間がかかるかもしれません。FSDの枠組み内には、トークンを保存できるいくつかの場所と、そのトークンをアプリケーションの他の部分で利用できるようにするいくつかの方法があります。 ### Shared層に保存する[​](#shared層に保存する "この見出しへの直接リンク") このアプローチは、APIクライアントが`shared/api`に定義されている場合にうまく機能します。なぜなら、APIクライアントがトークンに自由にアクセスできるからです。クライアントが状態を持つようにするには、リアクティブストアを使用するか、単にモジュールレベルの変数を使用することができます。その後、`login()`/`logout()`関数内でこの状態を更新できます。 トークンの自動更新は、APIクライアント内のミドルウェアとして実装できます。これは、リクエストを行うたびに実行されます。例えば、次のようにすることができます。 * 認証し、アクセストークンとリフレッシュトークンを保存する * 認証を必要とするリクエストを行う * リクエストがアクセストークンの有効期限切れを示すステータスコードで失敗した場合、ストレージにリフレッシュトークンがあれば、更新リクエストを行い、新しいアクセストークンとリフレッシュトークンを保存し、元のリクエストを再試行する このアプローチの欠点の1つは、トークンの保存と更新ロジックが専用の場所を持たないことです。これは、特定のアプリケーションやチームには適しているかもしれませんが、トークン管理のロジックがより複雑な場合、リクエスト送信とトークン管理の責任を分けたいと思うかもしれません。この場合、リクエストとAPIクライアントを`shared/api`に置き、トークンストレージと更新ロジックを`shared/auth`に配置します。 このアプローチのもう1つの欠点は、サーバーがトークンとともに現在のユーザーに関する情報を返す場合、その情報を保存する場所がなく、特別なエンドポイント(例えば`/me`や`/users/current`)から再度取得する必要があることです。 ### Entities層に保存する[​](#entities層に保存する "この見出しへの直接リンク") FSDプロジェクトには、ユーザーエンティティや現在のユーザーエンティティが存在することがよくあります。これらは同じエンティティである場合もあります。 注記 **現在のユーザー**は時には「viewer」や「me」とも呼ばれます。これは、権限とプライベート情報を持つ認証されたユーザーと、公開情報を持つ他のすべてのユーザーを区別するために行われます。 ユーザーエンティティにトークンを保存するには、`model`セグメントにリアクティブストアを作成します。このストアには、トークンとユーザー情報のオブジェクトの両方を含めることができます。 APIクライアントは通常、`shared/api`に配置されるか、エンティティ間で分散されるため、このアプローチの主な問題は、他のリクエストがトークンにアクセスできるようにしつつ、[レイヤーのインポートルール](/documentation/ja/docs/reference/layers.md#import-rule-on-layers)を破らないことです。 > スライス内のモジュール(ファイル)は、下層にあるスライスのみをインポートできる。 この問題にはいくつかの解決策があります。 1. **リクエストを行うたびにトークンを手動で渡す**
これは最も簡単な解決策ですが、すぐに不便になり、厳密な型付けがない場合は忘れやすくなります。この解決策は、Shared層のAPIクライアントのミドルウェアパターンとも互換性がありません。 2. **コンテキストや`localStorage`のようなグローバルストレージを介してアプリ全体にトークンへのアクセスを提供する**
トークンを取得するためのキーは`shared/api`に保存され、APIクライアントがそれを使用できるようにします。トークンのリアクティブストアはユーザーエンティティからエクスポートされ、必要に応じてコンテキストプロバイダーがApp層で設定されます。これにより、APIクライアントの設計に対する自由度が増しますが、このアプローチは暗黙の依存関係を生み出してしまいます。 3. **トークンが変更されるたびにAPIクライアントにトークンを挿入する**
リアクティブなストアであれば、変更を監視し、ユーザーエンティティのストアが変更されるたびにAPIクライアントのトークンを更新できます。この解決策は、前の解決策と同様に暗黙の依存関係を生み出してしまいますが、より命令的(「プッシュ」)であり、前のものはより宣言的(「プル」)です。 ユーザーエンティティのモデルに保存されたトークンの可用性の問題を解決したら、トークン管理に関連する追加のビジネスロジックを記述できます。例えば、`model`セグメントには、トークンを一定期間後に無効にするロジックや、期限切れのトークンを更新するロジックを含めることができます。これらのタスクを実行するために、ユーザーエンティティの`api`セグメント、または`shared/api`を使用します。 ### Pages層/Widgets層に保存する(非推奨)[​](#pages層widgets層に保存する非推奨 "この見出しへの直接リンク") アクセストークンのようなアプリ全体に関連する状態をページやウィジェットに保存することは推奨されません。ログインページの`model`セグメントにトークンストレージを配置しないでください。代わりに、最初の2つの解決策(Shared層配置かEntities層配置)のいずれかを選択してください。 ## ログアウトとトークンの無効化[​](#ログアウトとトークンの無効化 "この見出しへの直接リンク") 通常、アプリケーションではログアウト専用のページを作成しませんが、ログアウト機能は非常に重要です。この機能には、バックエンドへの認証リクエストとトークンストレージの更新が含まれます。 すべてのリクエストを`shared/api`に保存している場合は、ログイン関数の近くにログアウトリクエストの関数を配置してください。そうでない場合は、ログアウトを呼び出すボタンの近くに配置してください。例えば、すべてのページに存在し、ログアウトリンクを含むヘッダーウィジェットがある場合、そのリクエストをこのウィジェットの`api`セグメントに配置します。 トークンストレージの更新も、ログアウトボタンの場所からトリガーされる必要があります。リクエストとストレージの更新をこのウィジェットの`model`セグメントで統合できます。 ### 自動ログアウト[​](#自動ログアウト "この見出しへの直接リンク") ログアウトリクエストやトークン更新リクエストの失敗を考慮することを忘れないでください。いずれの場合も、トークンストレージをリセットする必要があります。トークンをEntities層に保存している場合、このコードは`model`セグメントに配置できます。トークンをShared層に保存している場合、このロジックを`shared/api`に配置すると、セグメントが膨らみ、その目的が曖昧になってしまいます。`api`セグメントに無関係な2つのものが含まれていることに気づいた場合、トークン管理ロジックを別のセグメント、例えば`shared/auth`に分離することを検討してみてください。 --- # 自動補完 WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/170) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # ブラウザAPI WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/197) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # CMS WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/172) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # フィードバック WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/187) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # i18n WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/171) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # メトリクス WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/181) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # モノレポ WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/221) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # ページレイアウト このガイドでは、複数のページが同じ構造を持ち、主な内容だけが異なる場合のページレイアウトの抽象化について説明します。 備考 あなたの質問がこのガイドにない場合は、この記事にフィードバックを残して質問を投稿してください(右側の青いボタン)、私たちはこのガイドを拡張する可能性を検討します! ## シンプルなレイアウト[​](#シンプルなレイアウト "この見出しへの直接リンク") 最もシンプルなレイアウトは、このページで直接見ることができます。これは、サイトのナビゲーションを含むヘッダー、2つのサイドバー、外部リンクを含むフッターを持っています。ここには複雑なビジネスロジックはなく、唯一の動的部分はサイドバーとヘッダーの右側にあるトグルスイッチです。このレイアウトは、`shared/ui`または`app/layouts`に全体を配置でき、サイドバーのコンテンツはプロパティを通じて埋め込むことができます。 shared/ui/layout/Layout.tsx ``` import { Link, Outlet } from "react-router-dom"; import { useThemeSwitcher } from "./useThemeSwitcher"; export function Layout({ siblingPages, headings }) { const [theme, toggleTheme] = useThemeSwitcher(); return (
{/* ここにページの主な内容が表示されます */}
  • GitHub
  • X
); } ``` shared/ui/layout/useThemeSwitcher.ts ``` export function useThemeSwitcher() { const [theme, setTheme] = useState("light"); function toggleTheme() { setTheme(theme === "light" ? "dark" : "light"); } useEffect(() => { document.body.classList.remove("light", "dark"); document.body.classList.add(theme); }, [theme]); return [theme, toggleTheme] as const; } ``` サイドバーのコードは読者に課題として残されています! ## レイアウトでのウィジェットの使用[​](#レイアウトでのウィジェットの使用 "この見出しへの直接リンク") 時には、特定のビジネスロジックをレイアウトに組み込む必要があります。特に、[React Router](https://reactrouter.com/)のような深くネストされたルートを使用している場合、Shared層やWidgets層にレイアウトを保存することはできません。これは[レイヤーのインポートルール](/documentation/ja/docs/reference/layers.md#import-rule-on-layers)に違反しています。 > スライス内のモジュールは、下層にあるスライスのみをインポートできる。 解決策を議論する前に、これが実際に問題かどうかを確認する必要があります。このレイアウトは本当に必要なのか?もしそうなら、ウィジェットとして実装することが最適なのかも再考する必要があるでしょう。もしビジネスロジックのブロックが2〜3ページで使用され、レイアウトがそのウィジェットの小さなラッパーに過ぎない場合、次の2つのオプションを検討してください。 1. **レイアウトをApp層のルーターで直接作成する**
これは、ネストをサポートするルーターに最適です。特定のルートをグループ化し、必要なレイアウトをそれらにのみ適用できます。 2. **単にコピーする**
コードを抽象化する欲求はしばしば過大評価されます。特にレイアウトに関しては、変更がほとんどないためです。ある時点で、これらのページの1つが変更を必要とする場合、他のページに影響を与えずに変更を加えることができます。他のページを更新することを忘れるかもしれないと心配している場合は、ページ間の関係を説明するコメントを残すことができます。 上記のいずれのオプションも適用できない場合、ウィジェットをレイアウトに組み込むための2つの解決策があります。 1. **レンダープロップまたはスロットを使用する**
ほとんどのフレームワークは、UIの一部を外部から渡すことを許可しています。Reactではこれを[レンダープロップ](https://www.patterns.dev/react/render-props-pattern/)と呼び、Vueでは[スロット](https://jp.vuejs.org/guide/components/slots)と呼びます。 2. **レイアウトをApp層に移動する**
レイアウトをApp層に保存し、必要なウィジェットを組み合わせることもできます。 ## 追加資料[​](#追加資料 "この見出しへの直接リンク") * ReactとRemixを使用した認証付きレイアウトの作成例は[チュートリアル](/documentation/ja/docs/get-started/tutorial.md)で見つけることができます(React Routerに類似する)。 --- # デスクトップ/タッチプラットフォーム WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/198) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # SSR WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/173) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # テーマ化 WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/207) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # 型 このガイドでは、TypeScriptのような型付き言語のデータの型と、それがFSDにどのように適合するかについて説明します。 備考 あなたの質問がこのガイドにない場合は、この記事にフィードバックを残して質問を投稿してください(右側の青いボタン)、私たちはこのガイドを拡張する可能性を検討します! ## ユーティリティ型[​](#ユーティリティ型 "この見出しへの直接リンク") ユーティリティ型は、特に意味を持たず、通常は他の型と一緒に使用される型です。例えば ``` type ArrayValues = T[number]; ``` 出典: ユーティリティ型をプロジェクトに追加するには、[`type-fest`](https://github.com/sindresorhus/type-fest)のようなライブラリをインストールするか、`shared/lib`に独自のライブラリを作成します。必ず、新しい型をこのライブラリに追加できるか、できないかを明確に示してください。例えば、`shared/lib/utility-types`と名付け、その中にユーティリティ型があなたのチームの理解において何であるかを説明するREADMEファイルを追加してください。 ユーティリティ型の再利用の可能性を過大評価しないでください。再利用可能であるからといって、必ずしも再利用されるわけではなく、したがってすべてのユーティリティ型がShared層に存在する必要はありません。一部のユーティリティ型は、使用される場所の近くに置くべきです。 * 📂 pages * 📂 home * 📂 api * 📄 ArrayValues.ts (ユーティリティ型) * 📄 getMemoryUsageMetrics.ts (このユーティリティを使用するコード) 警告 `shared/types`フォルダーを作成したり、スライスに`types`セグメントを追加する誘惑に負けないでください。「型」というカテゴリは「コンポーネント」や「フック」と同様に、内容を説明するものであり、目的を示すものではありません。セグメントはコードの目的を説明するべきであり、その本質を説明するべきではありません。 ## ビジネスエンティティと相互参照[​](#ビジネスエンティティと相互参照 "この見出しへの直接リンク") アプリケーションで最も重要な型の一つは、ビジネスエンティティの型、つまりアプリケーションが扱う実際のオブジェクトです。例えば、オンライン音楽サービスのアプリケーションでは、ビジネスエンティティとして「曲」(song)や「アルバム」(album)などがあります。 ビジネスエンティティは、しばしばバックエンドから提供されるため、最初のステップはバックエンドのレスポンスを型付けすることです。各エンドポイントに対してリクエスト関数を持ち、その関数の呼び出し結果を型付けするのが便利です。型の安全性を高めるために、Zodのようなスキーマ検証ライブラリを通じて結果を通過させることができます。 例えば、すべてのリクエストをShared層に保存している場合、次のようにできます。 shared/api/songs.ts ``` import type { Artist } from "./artists"; interface Song { id: number; title: string; artists: Array; } export function listSongs() { return fetch('/api/songs').then((res) => res.json() as Promise>); } ``` `Song`型が他の`Artist`エンティティを参照していることに気付くかもしれません。これはリクエストをShared層に保存する利点です。実際の型が相互に参照されることが多いです。この関数を`entities/song/api`に置いた場合、`entities/artist`から`Artist`を単純にインポートすることはできません。なぜなら、FSDはスライス間のクロスインポートを[レイヤーのインポートルール](/documentation/ja/docs/reference/layers.md#import-rule-on-layers)によって制限しているからです。 > スライス内のモジュールは、下層にあるスライスのみをインポートできる。 この問題を解決する方法は2つあります。 1. **型をパラメーター化する** 型が他のエンティティと接続するためのスロットとして型引数を受け取るようにすることができます。さらに、これらのスロットに制約を課すこともできます。例えば entities/song/model/song.ts ``` interface Song { id: number; title: string; artists: Array; } ``` これはいくつかの型に対してはうまく機能しますが、機能しないケースもあります。`Cart = { items: Array }`のような単純な型は、任意のプロダクト型で簡単に機能させることができます。しかし、`Country`と`City`のようなより関連性の高い型は、分離するのが難しいかもしれません。 2. **クロスインポートする(正しく)** FSD内でエンティティ間のクロスインポートを行うには、各スライス専用の特別の公開APIを使用することができます。例えば、`song`(曲)、`artist`(アーティスト)、`playlist`(プレイリスト)のエンティティがあり、後者の2つが`song`を参照する必要がある場合、`@x`ノーテーションを通じて`song`エンティティ内に2つの特別な公開APIを作成できます。 * 📂 entities * 📂 song * 📂 @x * 📄 artist.ts (公開API、`artist`エンティティをインポートする) * 📄 playlist.ts (公開API、`playlist`エンティティをインポートする) * 📄 index.ts (通常の公開API) `📄 entities/song/@x/artist.ts`ファイルの内容は、`📄 entities/song/index.ts`と似ています。 entities/song/@x/artist.ts ``` export type { Song } from "../model/song.ts"; ``` その後、`📄 entities/artist/model/artist.ts`は次のように`Song`をインポートできます。 entities/artist/model/artist.ts ``` import type { Song } from "entities/song/@x/artist"; export interface Artist { name: string; songs: Array; } ``` エンティティ間の明示的な関係を持つことで、依存関係を正確に制御し、ドメインの分離を十分に保つことができます。 ## データ転送オブジェクト(DTO)とマッパー[​](#data-transfer-objects-and-mappers "この見出しへの直接リンク") データ転送オブジェクト、またはDTO(Data Transfer Object)は、バックエンドから送信されるデータの形式を説明する用語です。時にはDTOをそのまま使用できますが、時にはその形式がフロントエンドにとって不便な場合があります。ここでマッパーが役立ちます。マッパーは、DTOをより使いやすい形式に変換する関数です。 ### DTOをどこに置くか[​](#dtoをどこに置くか "この見出しへの直接リンク") バックエンドの型が別のパッケージにある場合(例えば、フロントエンドとバックエンド間でコードを共有している場合)、そこからDTOをインポートするだけで済みます。バックエンドとフロントエンド間でコードを共有していない場合、DTOをフロントエンドコードのどこかに保存する必要があります。この場合については以下で説明します。 リクエスト関数を`shared/api`に保存している場合、その関数で使用するDTOも、ちょうどその関数の近くに配置するべきです。 shared/api/songs.ts ``` import type { ArtistDTO } from "./artists"; interface SongDTO { id: number; title: string; artist_ids: Array; } export function listSongs() { return fetch('/api/songs').then((res) => res.json() as Promise>); } ``` 前のセクションで述べたように、リクエストとDTOをShared層に保存する利点は、他のDTOを参照できることです。 ### マッパーをどこに置くか[​](#マッパーをどこに置くか "この見出しへの直接リンク") マッパーは、DTOを変換するための関数であり、したがってDTOの定義の近くに置くべきです。実際には、リクエストとDTOが`shared/api`に定義されている場合、マッパーもそこに置くべきです。 shared/api/songs.ts ``` import type { ArtistDTO } from "./artists"; interface SongDTO { id: number; title: string; disc_no: number; artist_ids: Array; } interface Song { id: string; title: string; /** 曲の完全なタイトル、ディスク番号を含む。 */ fullTitle: string; artistIds: Array; } function adaptSongDTO(dto: SongDTO): Song { return { id: String(dto.id), title: dto.title, fullTitle: `${dto.disc_no} / ${dto.title}`, artistIds: dto.artist_ids.map(String), }; } export function listSongs() { return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO)); } ``` リクエストとストレージがエンティティスライスに定義されている場合、このすべてのコードはそこに置くべきであり、エンティティ間のクロスインポートの制限を考慮する必要があります。 entities/song/api/dto.ts ``` import type { ArtistDTO } from "entities/artist/@x/song"; export interface SongDTO { id: number; title: string; disc_no: number; artist_ids: Array; } ``` entities/song/api/mapper.ts ``` import type { SongDTO } from "./dto"; export interface Song { id: string; title: string; /** 曲の完全なタイトル、ディスク番号を含む。 */ fullTitle: string; artistIds: Array; } export function adaptSongDTO(dto: SongDTO): Song { return { id: String(dto.id), title: dto.title, fullTitle: `${dto.disc_no} / ${dto.title}`, artistIds: dto.artist_ids.map(String), }; } ``` entities/song/api/listSongs.ts ``` import { adaptSongDTO } from "./mapper"; export function listSongs() { return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO)); } ``` entities/song/model/songs.ts ``` import { createSlice, createEntityAdapter } from "@reduxjs/toolkit"; import { listSongs } from "../api/listSongs"; export const fetchSongs = createAsyncThunk('songs/fetchSongs', listSongs); const songAdapter = createEntityAdapter(); const songsSlice = createSlice({ name: "songs", initialState: songAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => { builder.addCase(fetchSongs.fulfilled, (state, action) => { songAdapter.upsertMany(state, action.payload); }) }, }); ``` ### ネストされたDTOをどう扱うか[​](#ネストされたdtoをどう扱うか "この見出しへの直接リンク") 最も問題となるのは、バックエンドからのレスポンスが複数のエンティティを含む場合です。例えば、曲がアーティストのIDだけでなく、アーティストのデータオブジェクト全体を含む場合です。この場合、エンティティは互いに知らないわけにはいきません(データを捨てたり、バックエンドチームと真剣に話し合いたくない場合を除いて)。スライス間の暗黙的な関係の解決策を考えるのではなく、`@x`ノーテーションを通じて明示的なクロスインポートを選ぶべきです。Redux Toolkitを使用してこれを実装する方法は次のとおりです。 entities/song/model/songs.ts ``` import { createSlice, createEntityAdapter, createAsyncThunk, createSelector, } from '@reduxjs/toolkit' import { normalize, schema } from 'normalizr' import { getSong } from "../api/getSong"; // normalizrでエンティティのスキーマを宣言 export const artistEntity = new schema.Entity('artists') export const songEntity = new schema.Entity('songs', { artists: [artistEntity], }) const songAdapter = createEntityAdapter() export const fetchSong = createAsyncThunk( 'songs/fetchSong', async (id: string) => { const data = await getSong(id) // データを正規化して、リデューサーが予測可能なオブジェクトをロードできるようにします。例えば // `action.payload = { songs: {}, artists: {} }` const normalized = normalize(data, songEntity) return normalized.entities } ) export const slice = createSlice({ name: 'songs', initialState: songAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => { builder.addCase(fetchSong.fulfilled, (state, action) => { songAdapter.upsertMany(state, action.payload.songs) }) }, }) const reducer = slice.reducer export default reducer ``` entities/song/@x/artist.ts ``` export { fetchSong } from "../model/songs"; ``` entities/artist/model/artists.ts ``` import { createSlice, createEntityAdapter } from '@reduxjs/toolkit' import { fetchSong } from 'entities/song/@x/artist' const artistAdapter = createEntityAdapter() export const slice = createSlice({ name: 'users', initialState: artistAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => { builder.addCase(fetchSong.fulfilled, (state, action) => { // ここでバックエンドからの同じレスポンスを処理し、ユーザーを追加します artistAdapter.upsertMany(state, action.payload.artists) }) }, }) const reducer = slice.reducer export default reducer ``` これはスライスの分離の利点を少し制限しますが、私たちが制御できないこれらの2つのエンティティ間の関係を明確に示します。これらのエンティティがリファクタリングされる場合、同時にリファクタリングする必要があります。 ## グローバルの型とRedux[​](#グローバルの型とredux "この見出しへの直接リンク") グローバルの型とは、アプリケーション全体で使用される型のことです。グローバルの型には、必要な情報に応じて2種類があります。 1. アプリケーションに特有の情報を持たないユニバーサル型 2. アプリケーション全体について知る必要がある型 最初のケースは簡単に解決できます。型をShared層の適切なセグメントに置くだけです。例えば、分析用のグローバル変数のインターフェースがある場合、それを`shared/analytics`に置くことができます。 警告 `shared/types`フォルダーを作成することは避けてください。これは「型である」という特性に基づいて無関係なものをグループ化するだけであり、この特性はプロジェクト内でコードを検索する際には通常無意味です。 2番目のケースは、RTKなしでReduxを使用しているプロジェクトでよく見られます。最終的なストアの型は、すべてのリデューサーを結合した後にのみ利用可能ですが、このストアの型はアプリケーションで使用されるセレクターに必要です。例えば、以下はReduxでのストアの典型的な定義です。 app/store/index.ts ``` import { combineReducers, rootReducer } from "redux"; import { songReducer } from "entities/song"; import { artistReducer } from "entities/artist"; const rootReducer = combineReducers(songReducer, artistReducer); const store = createStore(rootReducer); type RootState = ReturnType; type AppDispatch = typeof store.dispatch; ``` `shared/store`に型付けされた`useAppDispatch`と`useAppSelector`のフックを持つことは良いアイデアですが、[レイヤーのインポートルール](/documentation/ja/docs/reference/layers.md#import-rule-on-layers)のために、App層から`RootState`と`AppDispatch`をインポートすることはできません。 > スライス内のモジュールは、下層にあるスライスのみをインポートできる。 この場合の推奨解決策は、Shared層とApp層の間に暗黙の依存関係を作成することです。これらの2つの型、`RootState`と`AppDispatch`は、変更される可能性が低く、Reduxの開発者には馴染みのあるものであるため、暗黙の関係は問題にならないでしょう。 TypeScriptでは、これらの型をグローバルとして宣言することで実現できます。例えば app/store/index.ts ``` /* 上記のコードブロックと同じ内容… */ declare type RootState = ReturnType; declare type AppDispatch = typeof store.dispatch; ``` shared/store/index.ts ``` import { useDispatch, useSelector, type TypedUseSelectorHook } from "react-redux"; export const useAppDispatch = useDispatch.withTypes() export const useAppSelector: TypedUseSelectorHook = useSelector; ``` ## 型のバリデーションスキーマとZod[​](#型のバリデーションスキーマとzod "この見出しへの直接リンク") データが特定の形式や制約に従っていることを確認したい場合、バリデーションスキーマを作成できます。TypeScriptでこの目的に人気のあるライブラリは[Zod](https://zod.dev)です。バリデーションスキーマは、可能な限りそれを使用するコードの近くに配置する必要があります。 バリデーションスキーマは、データ転送オブジェクト(DTO)と似ており([DTOとマッパー](#data-transfer-objects-and-mappers)のセクションで説明)、DTOを受け取り、それを解析し、解析に失敗した場合はエラーを返します。 バリデーションの最も一般的なケースの一つは、バックエンドからのデータです。通常、データがスキーマに従わない場合、リクエストを失敗としてマークしたいので、リクエスト関数と同じ場所にスキーマを置くのが良いでしょう。通常、これは`api`セグメントになります。 ユーザー入力を介してデータが送信される場合、例えばフォームを通じて、バリデーションはデータ入力時に行わなければなりません。この場合、スキーマを`ui`セグメントに配置し、フォームコンポーネントの近くに置くか、`ui`セグメントが過負荷である場合は`model`セグメントに配置できます。 ## コンポーネントのプロップスとコンテキストの型付け[​](#コンポーネントのプロップスとコンテキストの型付け "この見出しへの直接リンク") 一般的に、プロップスやコンテキストのインターフェースは、それを使用するコンポーネントやコンテキストと同じファイルに保存するのが最良です。VueやSvelteのように、単一ファイルコンポーネントを持つフレームワークの場合、インターフェースを同じファイルに定義できない場合や、複数のコンポーネント間でこのインターフェースを再利用したい場合は、通常は`ui`セグメント内の同じフォルダーに別のファイルを作成します。 以下はJSX(ReactまたはSolid)の例です。 pages/home/ui/RecentActions.tsx ``` interface RecentActionsProps { actions: Array<{ id: string; text: string }>; } export function RecentActions({ actions }: RecentActionsProps) { /* … */ } ``` 以下は、Vueのために別のファイルにインターフェースを保存する例です。 pages/home/ui/RecentActionsProps.ts ``` export interface RecentActionsProps { actions: Array<{ id: string; text: string }>; } ``` pages/home/ui/RecentActions.vue ``` ``` ## 環境宣言ファイル(`*.d.ts`)[​](#環境宣言ファイルdts "この見出しへの直接リンク") 一部のパッケージ、例えば[Vite](https://vitejs.dev)や[ts-reset](https://www.totaltypescript.com/ts-reset)は、アプリケーションで動作するために環境宣言ファイルを必要とします。通常、これらは小さくて簡単なので、特にアーキテクチャを必要とせず、単に`src/`に置くことができます。`src`をより整理されたものにするために、App層の`app/ambient/`に保存することもできます。 他のパッケージは単に型を持たず、その型を未定義として宣言する必要があるか、あるいは自分で型を作成する必要があるかもしれません。これらの型の良い場所は`shared/lib`で、`shared/lib/untyped-packages`のようなフォルダーです。そこに`%LIBRARY_NAME%.d.ts`というファイルを作成し、必要な型を宣言します。 shared/lib/untyped-packages/use-react-screenshot.d.ts ``` // このライブラリには型がなく、自分で型を書くのは億劫です。 declare module "use-react-screenshot"; ``` ## 型の自動生成[​](#型の自動生成 "この見出しへの直接リンク") 外部ソースから型を生成することは、しばしば便利です。例えば、OpenAPIスキーマからバックエンドの型を生成することができます。この場合、これらの型のためにコード内に特別な場所を作成します。例えば、`shared/api/openapi`のようにします。これらのファイルが何であるか、どのように再生成されるかを説明するREADMEをこのフォルダーに含めておくと理想的です。 --- # ホワイトラベル WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/215) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* --- # クロスインポート WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/220) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > クロスインポートは、レイヤーや抽象化が本来の責任以上に多くの責任を持ち始めると発生する。そのため、FSDは新しいレイヤーを設けて、これらのクロスインポートを分離することを可能にしている。 --- # デセグメンテーション WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/148) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## 状況[​](#situation "この見出しへの直接リンク") プロジェクトでは、特定のドメインに関連するモジュールが過度にデセグメント化され、プロジェクト全体に散らばっていることがよくあります。 ``` ├── components/ | ├── DeliveryCard | ├── DeliveryChoice | ├── RegionSelect | ├── UserAvatar ├── actions/ | ├── delivery.js | ├── region.js | ├── user.js ├── epics/ | ├── delivery.js | ├── region.js | ├── user.js ├── constants/ | ├── delivery.js | ├── region.js | ├── user.js ├── helpers/ | ├── delivery.js | ├── region.js | ├── user.js ├── entities/ | ├── delivery/ | | ├── getters.js | | ├── selectors.js | ├── region/ | ├── user/ ``` ## 問題[​](#problem "この見出しへの直接リンク") 問題は、**高い凝集性**の原則の違反と、**変更の軸**の過度な拡張として現れます。 ## 無視する場合[​](#if-you-ignore-it "この見出しへの直接リンク") * 例えば、配達に関するロジックに触れる必要がある場合、このロジックが複数の箇所に分散していることを考慮しなければならず、コード内で複数の箇所に触れる必要がある。これにより、**変更の軸**が過度に引き伸ばされる * ユーザーに関するロジックを調べる必要がある場合、**actions、epics、constants、entities、components**の詳細を調べるためにプロジェクト全体を巡回しなければならない * 暗黙関係と拡大するドメインの制御不能 * このアプローチでは、視野が狭くなり、「定数のための定数」を作成し、プロジェクトの該当ディレクトリをごちゃごちゃさせてしまうことに気づかないことがよくある ## 解決策[​](#solution "この見出しへの直接リンク") 特定のドメイン/ユースケースに関連するすべてのモジュールを近くに配置することです。 これは特定のモジュールを調べる際に、そのすべての構成要素がプロジェクト全体に散らばらず、近くに配置されるためです。 > これにより、コードベースとモジュール間の関係の発見しやすさと明確さが向上します。 ``` - ├── components/ - | ├── DeliveryCard - | ├── DeliveryChoice - | ├── RegionSelect - | ├── UserAvatar - ├── actions/ - | ├── delivery.js - | ├── region.js - | ├── user.js - ├── epics/{...} - ├── constants/{...} - ├── helpers/{...} ├── entities/ | ├── delivery/ + | | ├── ui/ # ~ components/ + | | | ├── card.js + | | | ├── choice.js + | | ├── model/ + | | | ├── actions.js + | | | ├── constants.js + | | | ├── epics.js + | | | ├── getters.js + | | | ├── selectors.js + | | ├── lib/ # ~ helpers | ├── region/ | ├── user/ ``` ## 参照[​](#see-also "この見出しへの直接リンク") * [(記事) Cohesion and Coupling: the difference](https://enterprisecraftsmanship.com/posts/cohesion-coupling-difference/) --- # ルーティング WIP この記事は執筆中です その公開を早めるために、以下の方法があります。 * 📢 フィードバックを共有する [(チケットでのコメント/絵文字リアクション)](https://github.com/feature-sliced/documentation/issues/169) * 💬 チャットでの議論結果をチケットにまとめる [(チャットURL)](https://t.me/feature_sliced) * ⚒️ 他の方法で[貢献する](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## 状況[​](#situation "この見出しへの直接リンク") ページへのURLが、pages層より下の層にハードコーディングされています。 entities/post/card ``` ... ``` ## 問題[​](#problem "この見出しへの直接リンク") URLがページ層に集中しておらず、責任範囲において適切な場所に配置されていません。 ## 無視する場合[​](#if-you-ignore-it "この見出しへの直接リンク") URLを変更する際に、URL(およびURL/リダイレクトのロジック)がpages層以外のすべての層に存在する可能性があることを考慮しなければなりません。 また、これは単純な商品カードでさえ、ページからの一部の責任を引き受けることを意味し、プロジェクト全体にロジックが分散してしまいます。 ## 解決策[​](#solution "この見出しへの直接リンク") URLやリダイレクトの処理をページ層およびそれ以上の層で定義することです。 URLを下層の層には、コンポジション/プロパティ/ファクトリーを通じて渡すことができます。 --- # カスタムアーキテクチャからの移行 このガイドは、カスタムのアーキテクチャからFeature-Sliced Designへの移行に役立つアプローチを説明します。 以下は、典型的なカスタムアーキテクチャのフォルダ構造です。このガイドでは、これを例として使用します。フォルダの内容が見えるように、フォルダの横にある青い矢印をクリックすることができます。 📁 src * 📁 actions * 📁 product * 📁 order * 📁 api * 📁 components * 📁 containers * 📁 constants * 📁 i18n * 📁 modules * 📁 helpers * 📁 routes * 📁 products.jsx * 📄 products.\[id].jsx * 📁 utils * 📁 reducers * 📁 selectors * 📁 styles * 📄 App.jsx * 📄 index.js ## 開始前に[​](#before-you-start "この見出しへの直接リンク") Feature-Sliced Designへの移行を検討する際に、チームに最も重要な質問は「本当に必要か?」です。私たちはFeature-Sliced Designが好きですが、いくつかのプロジェクトはそれなしでうまくいけることを認めています。 移行を検討すべき理由はいくつかあります。 1. 新しいチームメンバーが生産的なレベルに達するのが難しいと不満を言う。 2. コードの一部を変更すると、**しばしば**他の無関係な部分が壊れる。 3. 巨大なコードベースのため、新しい機能を追加するのが難しい。 **同僚の意に反してFSDに移行することは避けてください**。たとえあなたがチームリーダーであっても、まずは同僚を説得し、移行の利点がコストを上回ることを理解させる必要があります。 また、アーキテクチャの変更は、瞬時には経営陣には見えないことを覚えておいてください。始める前に、経営陣が移行を支持していることを確認し、この移行がプロジェクトにどのように役立つかを説明してください。 ヒント プロジェクトマネージャーをFSDの有用性に納得させるためのアイデアをいくつか紹介します。 1. FSDへの移行は段階的に行えるため、新機能の開発を止めることはありません。 2. 良いアーキテクチャは、新しい開発者が生産性を達成するのにかかる時間を大幅に短縮できます。 3. FSDは文書化されたアーキテクチャであるため、チームは独自の文書を維持するために常に時間を費やす必要がありません。 *** もし移行を始める決断をした場合、最初に行うべきことは`📁 src`のエイリアスを設定することです。これは、後で上位フォルダを参照するのに便利です。以降のテキストでは、`@`を`./src`のエイリアスとして扱います。 ## ステップ1。コードをページごとに分割する[​](#divide-code-by-pages "この見出しへの直接リンク") ほとんどのカスタムアーキテクチャは、ロジックのサイズに関係なく、すでにページごとに分割されています。もし`📁 pages`がすでに存在する場合は、このステップをスキップできます。 もし`📁 routes`しかない場合は、`📁 pages`を作成し、できるだけ多くのコンポーネントコードを`📁 routes`から移動させてみてください。理想的には、小さなルートファイルと大きなページファイルがあることです。コードを移動させる際には、各ページのためのフォルダを作成し、その中にインデックスファイルを追加します。 注記 現時点では、ページ同士が互いにインポートすることは問題ありません。後でこれらの依存関係を解消するための別のステップがあります。今はページごとの明確な分割を確立することに集中します。 ルートファイル src/routes/products.\[id].js ``` export { ProductPage as default } from "@/pages/product" ``` ページのインデックスファイル src/pages/product/index.js ``` export { ProductPage } from "./ProductPage.jsx" ``` ページコンポーネントファイル src/pages/product/ProductPage.jsx ``` export function ProductPage(props) { return
; } ``` ## ステップ2。 ページ以外のすべてを分離する[​](#separate-everything-else-from-pages "この見出しへの直接リンク") `📁 src/shared`フォルダを作成し、`📁 pages`や`📁 routes`からインポートされないすべてをそこに移動します。`📁 src/app`フォルダを作成し、ページやルートをインポートするすべてをそこに移動します。 Shared層にはスライスがないことを覚えておいてください。したがって、セグメントは互いにインポートできます。 最終的には、次のようなファイル構造になるはずです。 📁 src * 📁 app * 📁 routes * 📄 products.jsx * 📄 products.\[id].jsx * 📄 App.jsx * 📄 index.js * 📁 pages * 📁 product * 📁 ui * 📄 ProductPage.jsx * 📄 index.js * 📁 catalog * 📁 shared * 📁 actions * 📁 api * 📁 components * 📁 containers * 📁 constants * 📁 i18n * 📁 modules * 📁 helpers * 📁 utils * 📁 reducers * 📁 selectors * 📁 styles ## ステップ3。 ページ間のクロスインポートを解消する[​](#tackle-cross-imports-between-pages "この見出しへの直接リンク") あるページが他のページから何かをインポートしているすべての箇所を見つけ、次のいずれかを行います。 1. インポートされているコードを依存するページにコピーして、依存関係を取り除く。 2. コードをShared層の適切なセグメントに移動する * UIキットの一部であれば、`📁 shared/ui`に移動。 * 設定の定数であれば、`📁 shared/config`に移動。 * バックエンドとのやり取りであれば、`📁 shared/api`に移動。 注記 **コピー自体はアーキテクチャの問題ではありません**。実際、時には新しい再利用可能なモジュールに何かを抽象化するよりも、何かを複製する方が正しい場合もあります。ページの共通部分が異なってくることがあるため、その場合、依存関係が妨げにならないようにする必要があります。 ただし、DRY("don't repeat yourself" — "繰り返さない")の原則には意味があるため、ビジネスロジックをコピーしないようにしてください。そうしないと、バグを複数の箇所で修正することになることを頭に入れておく必要があります。 ## ステップ4。 Shared層を分解する[​](#unpack-shared-layer "この見出しへの直接リンク") この段階では、Shared層に多くのものが入っているかもしれませんが、一般的にはそのような状況を避けるべきです。理由は、Shared層に依存している他の層にあるコードが存在している可能性があるため、そこに変更を加えることは予期しない結果を引き起こす可能性が高くなります。 特定のページでのみ使用されるすべてのオブジェクトを見つけ、それらをそのページのスライスに移動します。そして、*これにはアクション、リデューサー、セレクターも含まれます*。すべてのアクションを一緒にグループ化することには意味がありませんが、関連するアクションをその使用場所の近くに置くことには意味があります。 最終的には、次のようなファイル構造になるはずです。 📁 src * 📁 app (変更なし) * 📁 pages * 📁 product * 📁 actions * 📁 reducers * 📁 selectors * 📁 ui * 📄 Component.jsx * 📄 Container.jsx * 📄 ProductPage.jsx * 📄 index.js * 📁 catalog * 📁 shared (再利用されるオブジェクトのみ) * 📁 actions * 📁 api * 📁 components * 📁 containers * 📁 constants * 📁 i18n * 📁 modules * 📁 helpers * 📁 utils * 📁 reducers * 📁 selectors * 📁 styles ## ステップ5。 コードを技術的な目的に基づいて整理する[​](#organize-by-technical-purpose "この見出しへの直接リンク") FSDでは、技術的な目的に基づく分割が*セグメント*によって行われます。よく見られるセグメントはいくつかあります。 * `ui` — インターフェースの表示に関連するすべて: UIコンポーネント、日付のフォーマット、スタイルなど。 * `api` — バックエンドとのやり取り: リクエスト関数、データ型、マッパーなど。 * `model` — データモデル: スキーマ、インターフェース、ストレージ、ビジネスロジック。 * `lib` — 他のモジュールに必要なライブラリコード。 * `config` — 設定ファイルやフィーチャーフラグ。 必要に応じて独自のセグメントを作成できます。ただし、コードをその性質によってグループ化するセグメント(例: `components`、`actions`、`types`、`utils`)を作成しないようにしてください。代わりに、コードの目的に基づいてグループ化してください。 ページのコードをセグメントに再分配します。すでに`ui`セグメントがあるはずなので、今は他のセグメント(例えば、アクション、リデューサー、セレクターのための`model`や、サンクやミューテーションのための`api`)を作成するときです。 また、Shared層を再分配して、次のフォルダを削除します。 * `📁 components`、`📁 containers` — その内容のほとんどは`📁 shared/ui`になるべきです。 * `📁 helpers`、`📁 utils` — 再利用可能なヘルパー関数が残っている場合は、目的に基づいてグループ化し、これらのグループを`📁 shared/lib`に移動します。 * `📁 constants` — 同様に、目的に基づいてグループ化し、`📁 shared/config`に移動します。 ## 任意のステップ[​](#optional-steps "この見出しへの直接リンク") ### ステップ6。 複数のページで使用されるReduxスライスからエンティティ/フィーチャーを形成する[​](#form-entities-features-from-redux "この見出しへの直接リンク") 通常、これらの再利用可能なReduxスライスは、ビジネスに関連する何かを説明します(例えば、プロダクトやユーザーなど)。したがって、それらをエンティティ層に移動できます。1つのエンティティにつき1つのフォルダです。Reduxスライスが、ユーザーがアプリケーションで実行したいアクションに関連している場合(例えば、コメントなど)、それをフィーチャー層に移動できます。 エンティティとフィーチャーは互いに独立している必要があります。ビジネス領域に組み込まれたエンティティ間の関係がある場合は、[ビジネスエンティティに関するガイド](/documentation/ja/docs/guides/examples/types.md#business-entities-and-their-cross-references)を参照して、これらの関係を整理する方法を確認してください。 これらのスライスに関連するAPI関数は、`📁 shared/api`に残すことができます。 ### ステップ7。 モジュールをリファクタリングする[​](#refactor-your-modules "この見出しへの直接リンク") `📁 modules`フォルダは通常、ビジネスロジックに使用されるため、すでにFSDのフィーチャー層に似た性質を持っています。一部のモジュールは、アプリケーションの大きな部分(例えば、アプリのヘッダーなど)を説明することもあります。この場合、それらをウィジェット層に移動できます。 ### ステップ8。 `shared/ui`にUI基盤を正しく形成する[​](#form-clean-ui-foundation "この見出しへの直接リンク") 理想的には、`📁 shared/ui`にはビジネスロジックが含まれていないUI要素のセットが含まれるべきです。また、非常に再利用可能である必要があります。 以前`📁 components`や`📁 containers`にあったUIコンポーネントをリファクタリングして、ビジネスロジックを分離します。このビジネスロジックを上位層に移動します。あまり多くの場所で使用されていない場合は、コピーを検討することもできます。 --- # v1からv2への移行 ## なぜv2なのか?[​](#why-v2 "この見出しへの直接リンク") 初期の**feature-slices**の概念は、2018年に提唱されました。 それ以来、FSD方法論は多くの変革を経てきましたが、基本的な原則は保持されています。 * *標準化された*フロントエンドプロジェクト構造の使用 * アプリケーションを*ビジネスロジック*に基づいて分割 * *孤立した機能*の使用により、暗黙の副作用や循環依存を防止 * モジュールの「内部」にアクセスすることを禁止する*公開API*の使用 しかし、以前のバージョンのFSD方法論には依然として**弱点が残っていました**。 * ボイラープレートの発生 * コードベースの過剰な複雑化と抽象化間の明確でないルール * プロジェクトのメンテナンスや新しいメンバーのオンボーディングを妨げていた暗黙のアーキテクチャ的決定 新しいバージョンのFSD方法論([v2](https://github.com/feature-sliced/documentation))は、**これらの欠点を解消しつつ、既存の利点を保持することを目的としています**。 2018年以降、[**feature-driven**](https://github.com/feature-sliced/documentation/tree/rc/feature-driven)という別の類似の方法論が[発展してきました](https://github.com/kof/feature-driven-architecture/issues)。それを最初に提唱したのは[Oleg Isonen](https://github.com/kof)でした。 2つのアプローチの統合により、**既存のプラクティスが改善され、柔軟性、明確さ、効率が向上しました**。 > 結果として、方法論の名称も「feature-slice**d**」に変更されました。 ## なぜプロジェクトをv2に移行する意味があるのか?[​](#why-does-it-make-sense-to-migrate-the-project-to-v2 "この見出しへの直接リンク") > `WIP:` 現在の方法論のバージョンは開発中であり、一部の詳細は*変更される可能性があります*。 #### 🔍 より透明でシンプルなアーキテクチャ[​](#-more-transparent-and-simple-architecture "この見出しへの直接リンク") FSD(v2)は、**より直感的で、開発者の間で広く受け入れられている抽象化とロジックの分割方法を提供しています**。 これにより、新しいメンバーの参加やプロジェクトの現状理解、アプリケーションのビジネスロジック分配に非常に良い影響を与えます。 #### 📦 より柔軟で誠実なモジュール性[​](#-more-flexible-and-honest-modularity "この見出しへの直接リンク") FSD(v2)は、**より柔軟な方法でロジックを分配することを可能にしています**。 * 孤立した部分をゼロからリファクタリングできる * 同じ抽象化に依存しつつ、余計な依存関係の絡みを避けられる * 新しいモジュールの配置をよりシンプルにできる *(layer → slice → segment)* #### 🚀 より多くの仕様、計画、コミュニティ[​](#-more-specifications-plans-community "この見出しへの直接リンク") `core-team`は最新の(v2)バージョンのFSD方法論に積極的に取り組んでいます。 したがって、以下のことが期待できます。 * より多くの記述されたケース/問題 * より多くの適用ガイド * より多くの実例 * 新しいメンバーのオンボーディングや方法論概念の学習のための全体的な文書の増加 * 方法論の概念とアーキテクチャに関するコンベンションを遵守するためのツールキットのさらなる発展 > もちろん、初版に対するユーザーサポートも行われますが、私たちにとっては最新のバージョンが最優先です。 > 将来的には、次のメジャーアップデートの際に、現在のバージョン(v2)へのアクセスが保持され、**チームやプロジェクトにリスクをもたらすことはありません**。 ## Changelog[​](#changelog "この見出しへの直接リンク") ### `BREAKING` Layers[​](#breaking-layers "この見出しへの直接リンク") FSD方法論は上位レベルでの層の明示的な分離を前提としています。 * `/app` > `/processes` > **`/pages`** > **`/features`** > `/entities` > `/shared` * *つまり、すべてがフィーチャーやページとして解釈されるわけではない* * このアプローチにより、層のルールを明示的に設定することが可能になる * モジュールの**層が高いほど**、より多くの**コンテキスト**を持つことができる *(言い換えれば、各層のモジュールは、下層のモジュールのみをインポートでき、上層のモジュールはインポートできない)* * モジュールの**層が低いほど**、変更を加える際の**危険性と責任**が増す *(一般的に、再利用されるのは下層のモジュールらからである)* ### `BREAKING` Shared層[​](#breaking-shared層 "この見出しへの直接リンク") 以前はプロジェクトのsrcルートにあったインフラストラクチャの `/ui`, `/lib`, `/api` 抽象化は、現在 `/src/shared` という別のディレクトリに分離されています。 * `shared/ui` - アプリケーションの共通UIキット(オプション) * *ここで`Atomic Design`を使用することは引き続き許可されている* * `shared/lib` - ロジックを実装するための補助ライブラリセット * *引き続き、ヘルパー関数の「ごみ屋敷」を作らずに* * `shared/api` - APIへのアクセスのための共通エントリポイント * *各フィーチャー/ページにローカルに記述することも可能だが、推奨されない* * 以前と同様に、`shared`にはビジネスロジックへの明示的な依存関係があってはならない * *必要に応じて、この依存関係は`entities`、またはそれ以上の層に移動する必要がある* ### `新規` Entities層, Processes層[​](#新規-entities層-processes層 "この見出しへの直接リンク") v2では、**ロジックの複雑さと強い結合の問題を解消するために、新しい抽象化が追加されました**。 * `/entities` - **ビジネスエンティティ**の層で、ビジネスモデルやフロントエンド専用の合成エンティティに関連するスライスを含む * *例:`user`, `i18n`, `order`, `blog`* * `/processes` - アプリケーション全体にわたる**ビジネスプロセス**の層 * **この層はオプションであり、通常は*ロジックが拡大し、複数のページにまたがる場合に使用が推奨される*** * *例:`payment`, `auth`, `quick-tour`* ### `BREAKING` 抽象化と命名[​](#breaking-抽象化と命名 "この見出しへの直接リンク") 具体的な抽象化とその命名に関する[明確なガイドライン](/documentation/ja/docs/about/understanding/naming.md)が定義されています。 #### Layers[​](#layers "この見出しへの直接リンク") * `/app` — **アプリケーションの初期化層** * *以前のバリエーション: `app`, `core`, `init`, `src/index`* * `/processes` — **ビジネスプロセスの層** * *以前のバリエーション: `processes`, `flows`, `workflows`* * `/pages` — **アプリケーションのページ層** * *以前のバリエーション: `pages`, `screens`, `views`, `layouts`, `components`, `containers`* * `/features` — **機能部分の層** * *以前のバリエーション: `features`, `components`, `containers`* * `/entities` — **ビジネスエンティティの層** * *以前のバリエーション: `entities`, `models`, `shared`* * `/shared` — **再利用可能なインフラストラクチャコードの層** 🔥 * *以前のバリエーション: `shared`, `common`, `lib`* #### Segments[​](#segments "この見出しへの直接リンク") * `/ui` — **UIセグメント** 🔥 * *以前のバリエーション:`ui`, `components`, `view`* * `/model` — **ビジネスロジックのセグメント** 🔥 * *以前のバリエーション:`model`, `store`, `state`, `services`, `controller`* * `/lib` — **補助コードのセグメント** * *以前のバリエーション:`lib`, `libs`, `utils`, `helpers`* * `/api` — **APIセグメント** * *以前のバリエーション:`api`, `service`, `requests`, `queries`* * `/config` — **アプリケーション設定のセグメント** * *以前のバリエーション:`config`, `env`, `get-env`* ### `REFINED` 低結合[​](#refined-低結合 "この見出しへの直接リンク") 新しいレイヤーのおかげで、モジュール間の[低結合の原則](/documentation/ja/docs/reference/slices-segments.md#zero-coupling-high-cohesion)を遵守することがはるかに簡単になりました。 *それでも、モジュールを「切り離す」ことが非常に難しい場合は、できるだけ避けることが推奨されます*。 ## 参照[​](#see-also "この見出しへの直接リンク") * [React Berlin Talk - Oleg Isonen "Feature Driven Architecture"](https://www.youtube.com/watch?v=BWAeYuWFHhs) --- # v2.0からv2.1への移行 v2.1の主な変更点は、インターフェースを分解するための「ページファースト」という新しいメンタルモデルです。 v2.0では、FSDは分解のためにエンティティ表現やインタラクティビティの最小部分まで考慮し、インターフェース内のエンティティとフィーチャーを特定することを推奨していました。そうしてから、エンティティとフィーチャーからウィジェットやページが構築されていきました。この分解モデルでは、ほとんどのロジックはエンティティとフィーチャーにあり、ページはそれ自体にはあまり重要性のない構成層に過ぎませんでした。 v2.1では、分解をページから始めること、または場合によってはページで止めることを推奨します。ほとんどの人はすでにアプリを個々のページに分ける方法を知っており、ページはコードベース内のコンポーネントを見つける際の一般的な出発点でもあります。この新しい分解モデルでは、各個別のページにほとんどのUIとロジックを保持し、Sharedに再利用可能な基盤を維持します。複数のページでビジネスロジックを再利用する必要が生じた場合は、それを下層のレイヤーに移動できます。 Feature-Sliced Designへのもう一つの追加は、`@x`表記を使用したエンティティ間のクロスインポートの標準化です。 ## 移行方法[​](#how-to-migrate "この見出しへの直接リンク") v2.1には破壊的な変更はなく、FSD v2.0で書かれたプロジェクトもFSD v2.1の有効なプロジェクトです。しかし、新しいメンタルモデルがチームや特に新しい開発者のオンボーディングにとってより有益であると考えているため、分解に対して小さな調整を行うことを推奨します。 ### スライスのマージ[​](#スライスのマージ "この見出しへの直接リンク") 移行を始めるための簡単な方法は、プロジェクトでFSDのリンターである[Steiger](https://github.com/feature-sliced/steiger)を実行することです。Steigerは新しいメンタルモデルで構築されており、最も役立つルールは次のとおりです。 * [`insignificant-slice`](https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/insignificant-slice) — エンティティ、またはフィーチャーが1ページでのみ使用されている場合、このルールはそのエンティティ、またはフィーチャーをページに完全にマージすることを提案します。 * [`excessive-slicing`](https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/excessive-slicing) — レイヤーにスライスが多すぎる場合、通常は分解が細かすぎるサインです。このルールは、プロジェクトのナビゲーションを助けるためにいくつかのスライスをマージ、またはグループ化することを提案します。 ``` npx steiger src ``` これにより、1回だけ使用されるスライスを特定できるため、それらが本当に必要か再考することができます。そのような考慮において、レイヤーはその内部のすべてのスライスのための何らかのグローバル名前空間を形成することを念頭に置いてください。1回だけ使用される変数でグローバル名前空間を汚染しないようにするのと同様に、レイヤーの名前空間内の場所を貴重なものとして扱い、慎重に使用するべきです。 ### クロスインポートの標準化[​](#クロスインポートの標準化 "この見出しへの直接リンク") 以前にプロジェクト内でクロスインポートがあった場合、Feature-Sliced Designでのクロスインポートのための新しい表記法`@x`を活用できます。これは次のようになります。 entities/B/some/file.ts ``` import type { EntityA } from "entities/A/@x/B"; ``` 詳細については、リファレンスの[クロスインポートの公開API](/documentation/ja/docs/reference/public-api.md#public-api-for-cross-imports)セクションを参照してください。 --- # Usage with Electron Electron applications have a special architecture consisting of multiple processes with different responsibilities. Applying FSD in such a context requires adapting the structure to the Electron specifics. ``` └── src ├── app # Common app layer │ ├── main # Main process │ │ └── index.ts # Main process entry point │ ├── preload # Preload script and Context Bridge │ │ └── index.ts # Preload entry point │ └── renderer # Renderer process │ └── index.html # Renderer process entry point ├── main │ ├── features │ │ └── user │ │ └── ipc │ │ ├── get-user.ts │ │ └── send-user.ts │ ├── entities │ └── shared ├── renderer │ ├── pages │ │ ├── settings │ │ │ ├── ipc │ │ │ │ ├── get-user.ts │ │ │ │ └── save-user.ts │ │ │ ├── ui │ │ │ │ └── user.tsx │ │ │ └── index.ts │ │ └── home │ │ ├── ui │ │ │ └── home.tsx │ │ └── index.ts │ ├── widgets │ ├── features │ ├── entities │ └── shared └── shared # Common code between main and renderer └── ipc # IPC description (event names, contracts) ``` ## Public API rules[​](#public-api-rules "この見出しへの直接リンク") Each process must have its own public API. For example, you can't import modules from `main` to `renderer`. Only the `src/shared` folder is public for both processes. It's also necessary for describing contracts for process interaction. ## Additional changes to the standard structure[​](#additional-changes-to-the-standard-structure "この見出しへの直接リンク") It's suggested to use a new `ipc` segment, where interaction between processes takes place. The `pages` and `widgets` layers, based on their names, should not be present in `src/main`. You can use `features`, `entities` and `shared`. The `app` layer in `src` contains entry points for `main` and `renderer`, as well as the IPC. It's not desirable for segments in the `app` layer to have intersection points ## Interaction example[​](#interaction-example "この見出しへの直接リンク") src/shared/ipc/channels.ts ``` export const CHANNELS = { GET_USER_DATA: 'GET_USER_DATA', SAVE_USER: 'SAVE_USER', } as const; export type TChannelKeys = keyof typeof CHANNELS; ``` src/shared/ipc/events.ts ``` import { CHANNELS } from './channels'; export interface IEvents { [CHANNELS.GET_USER_DATA]: { args: void, response?: { name: string; email: string; }; }; [CHANNELS.SAVE_USER]: { args: { name: string; }; response: void; }; } ``` src/shared/ipc/preload.ts ``` import { CHANNELS } from './channels'; import type { IEvents } from './events'; type TOptionalArgs = T extends void ? [] : [args: T]; export type TElectronAPI = { [K in keyof typeof CHANNELS]: (...args: TOptionalArgs) => IEvents[typeof CHANNELS[K]]['response']; }; ``` src/app/preload/index.ts ``` import { contextBridge, ipcRenderer } from 'electron'; import { CHANNELS, type TElectronAPI } from 'shared/ipc'; const API: TElectronAPI = { [CHANNELS.GET_USER_DATA]: () => ipcRenderer.sendSync(CHANNELS.GET_USER_DATA), [CHANNELS.SAVE_USER]: args => ipcRenderer.invoke(CHANNELS.SAVE_USER, args), } as const; contextBridge.exposeInMainWorld('electron', API); ``` src/main/features/user/ipc/send-user.ts ``` import { ipcMain } from 'electron'; import { CHANNELS } from 'shared/ipc'; export const sendUser = () => { ipcMain.on(CHANNELS.GET_USER_DATA, ev => { ev.returnValue = { name: 'John Doe', email: 'john.doe@example.com', }; }); }; ``` src/renderer/pages/user-settings/ipc/get-user.ts ``` import { CHANNELS } from 'shared/ipc'; export const getUser = () => { const user = window.electron[CHANNELS.GET_USER_DATA](); return user ?? { name: 'John Donte', email: 'john.donte@example.com' }; }; ``` ## See also[​](#see-also "この見出しへの直接リンク") * [Process Model Documentation](https://www.electronjs.org/docs/latest/tutorial/process-model) * [Context Isolation Documentation](https://www.electronjs.org/docs/latest/tutorial/context-isolation) * [Inter-Process Communication Documentation](https://www.electronjs.org/docs/latest/tutorial/ipc) * [Example](https://github.com/feature-sliced/examples/tree/master/examples/electron) --- # NextJSとの併用 NextJSプロジェクトでFSDを実装することは可能ですが、プロジェクトの構造に関するNextJSの要件とFSDの原則の間に2つの点で対立が生じます。 * `pages`のファイルルーティング * NextJSにおける`app`の対立、または欠如 ## `pages`におけるFSDとNextJSの対立[​](#pages-conflict "この見出しへの直接リンク") NextJSは、アプリケーションのルートを定義するために`pages`フォルダーを使用することを提案しています。`pages`フォルダー内のファイルがURLに対応することを期待しています。このルーティングメカニズムは、FSDの概念に**適合しません**。なぜなら、このようなルーティングメカニズムでは、スライスの平坦な構造を維持することができないからです。 ### NextJSの`pages`フォルダーをプロジェクトのルートフォルダーに移動する(推奨)[​](#nextjsのpagesフォルダーをプロジェクトのルートフォルダーに移動する推奨 "この見出しへの直接リンク") このアプローチは、NextJSの`pages`フォルダーをプロジェクトのルートフォルダーに移動し、FSDのページをNextJSの`pages`フォルダーにインポートすることにあります。これにより、`src`フォルダー内でFSDのプロジェクト構造を維持できます。 ``` ├── pages # NextJSのpagesフォルダー ├── src │ ├── app │ ├── entities │ ├── features │ ├── pages # FSDのpagesフォルダー │ ├── shared │ ├── widgets ``` ### FSD構造における`pages`フォルダーの名前変更[​](#fsd構造におけるpagesフォルダーの名前変更 "この見出しへの直接リンク") もう一つの解決策は、FSD構造内の`pages`層の名前を変更して、NextJSの`pages`フォルダーとの名前衝突を避けることです。 FSDの`pages`層を`views`層に変更することができます。 このようにすることで、`src`フォルダー内のプロジェクト構造は、NextJSの要件と矛盾することなく保持されます。 ``` ├── app ├── entities ├── features ├── pages # NextJSのpagesフォルダー ├── views # 名前が変更されたFSDのページフォルダー ├── shared ├── widgets ``` この場合、プロジェクトのREADMEや内部ドキュメントなど、目立つ場所にこの名前変更を文書化することをお勧めします。この名前変更は、[「プロジェクト知識」](/documentation/ja/docs/about/understanding/knowledge-types.md)の一部です。 ## NextJSにおける`app`フォルダーの欠如[​](#app-absence "この見出しへの直接リンク") NextJSのバージョン13未満では、明示的な`app`フォルダーは存在せず、代わりにNextJSは`_app.tsx`ファイルを提供しています。このファイルは、プロジェクトのすべてのページのラッピングコンポーネントとして機能しています。 ### `pages/_app.tsx`ファイルへの機能のインポート[​](#pages_apptsxファイルへの機能のインポート "この見出しへの直接リンク") NextJSの構造における`app`フォルダーの欠如の問題を解決するために、`app`層内に`App`コンポーネントを作成し、NextJSがそれを使用できるように`pages/_app.tsx`に`App`コンポーネントをインポートすることができます。例えば ``` // app/providers/index.tsx const App = ({ Component, pageProps }: AppProps) => { return ( ); }; export default App; ``` その後、`App`コンポーネントとプロジェクトのグローバルスタイルを`pages/_app.tsx`に次のようにインポートできます。 ``` // pages/_app.tsx import 'app/styles/index.scss' export { default } from 'app/providers'; ``` ## App Routerの使用[​](#app-router "この見出しへの直接リンク") App Routerは、Next.jsのバージョン13.4で安定版として登場しました。App Routerを使用すると、`pages`フォルダーの代わりに`app`フォルダーをルーティングに使用できます。 FSDの原則に従うために、NextJSの`app`フォルダーを`pages`フォルダーとの名前衝突を解消するために推奨される方法で扱うべきです。 このアプローチは、NextJSの`app`フォルダーをプロジェクトのルートフォルダーに移動し、FSDのページをNextJSの`app`フォルダーにインポートすることに基づいています。これにより、`src`フォルダー内のFSDプロジェクト構造が保持されます。また、プロジェクトのルートフォルダーに`pages`フォルダーを追加することもお勧めします。なぜなら、App RouterはPages Routerと互換性があるからです。 ``` ├── app # NextJSのappフォルダー ├── pages # 空のNextJSのpagesフォルダー │ ├── README.md # このフォルダーの目的に関する説明 ├── src │ ├── app # FSDのappフォルダー │ ├── entities │ ├── features │ ├── pages # FSDのpagesフォルダー │ ├── shared │ ├── widgets ``` [![StackBlitzで開く](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/edit/stackblitz-starters-aiez55?file=README.md) --- # NuxtJSとの併用 NuxtJSプロジェクトでFSDを実装することは可能ですが、NuxtJSのプロジェクト構造要件とFSDの原則の違いにより、以下の2点でコンフリクトが発生してしまいます。 * NuxtJSは`src`フォルダーなしでプロジェクトのファイル構造を提供している。つまり、ファイル構造がプロジェクトのルートに配置される。 * ファイルルーティングは`pages`フォルダーにあるが、FSDではこのフォルダーはフラットなスライス構造に割り当てられている。 ## `src`ディレクトリのエイリアスを追加する[​](#srcディレクトリのエイリアスを追加する "この見出しへの直接リンク") 設定ファイルに`alias`オブジェクトを追加します。 ``` export default defineNuxtConfig({ devtools: { enabled: true }, // FSDには関係なく、プロジェクト起動時に有効 alias: { "@": '../src' }, }) ``` ## ルーター設定方法の選択[​](#ルーター設定方法の選択 "この見出しへの直接リンク") NuxtJSには、コンフィグを使用する方法とファイル構造を使用する方法の2つのルーティング設定方法があります。 ファイルベースのルーティングの場合、`app/routes`ディレクトリ内に`index.vue`ファイルを作成します。一方、コンフィグを使用する場合は、`router.options.ts`ファイルでルートを設定します。 ### コンフィグによるルーティング[​](#コンフィグによるルーティング "この見出しへの直接リンク") `app`層に`router.options.ts`ファイルを作成し、設定オブジェクトをエクスポートします。 app/router.options.ts ``` import type { RouterConfig } from '@nuxt/schema'; export default { routes: (_routes) => [], }; ``` プロジェクトにホームページを追加するには、次の手順を行います。 * `pages`層内にページスライスを追加する * `app/router.config.ts`のコンフィグに適切なルートを追加する ページスライスを作成するには、[CLI](https://github.com/feature-sliced/cli)を使用します。 ``` fsd pages home ``` `home-page.vue`ファイルを`ui`セグメント内に作成し、公開APIを介してアクセスできるようにします。 src/pages/home/index.ts ``` export { default as HomePage } from './ui/home-page'; ``` このように、ファイル構造は次のようになります。 ``` |── src │ ├── app │ │ ├── router.config.ts │ ├── pages │ │ ├── home │ │ │ ├── ui │ │ │ │ ├── home-page.vue │ │ │ ├── index.ts ``` 最後に、ルートをコンフィグに追加します。 app/router.config.ts ``` import type { RouterConfig } from '@nuxt/schema' export default { routes: (_routes) => [ { name: 'home', path: '/', component: () => import('@/pages/home.vue').then(r => r.default || r) } ], } ``` ### ファイルルーティング[​](#ファイルルーティング "この見出しへの直接リンク") まず、プロジェクトのルートに`src`ディレクトリを作成し、その中に`app`層と`pages`層のレイヤー、`app`層内に`routes`フォルダーを作成します。 このように、ファイル構造は次のようになります。 ``` ├── src │ ├── app │ │ ├── routes │ ├── pages # FSDに割り当てられたpagesフォルダー ``` NuxtJSが`app`層内の`routes`フォルダーをファイルルーティングに使用するには、`nuxt.config.ts`を次のように変更します。 nuxt.config.ts ``` export default defineNuxtConfig({ devtools: { enabled: true }, // FSDには関係なく、プロジェクト起動時に有効 alias: { "@": '../src' }, dir: { pages: './src/app/routes' } }) ``` これで、`app`層内のページに対してルートを作成し、`pages`層からページを接続できます。 例えば、プロジェクトに`Home`ページを追加するには、次の手順を行います。 * `pages`層内にページスライスを追加する * `app`層内に適切なルートを追加する * スライスのページをルートに接続する ページスライスを作成するには、[CLI](https://github.com/feature-sliced/cli)を使用します。 ``` fsd pages home ``` `home-page.vue`ファイルを`ui`セグメント内に作成し、公開APIを介してアクセスできるようにします。  src/pages/home/index.ts ``` export { default as HomePage } from './ui/home-page'; ``` このページのルートを`app`層内に作成します。 ``` ├── src │ ├── app │ │ ├── routes │ │ │ ├── index.vue │ ├── pages │ │ ├── home │ │ │ ├── ui │ │ │ │ ├── home-page.vue │ │ │ ├── index.ts ``` `index.vue`ファイル内にページコンポーネントを追加します。 src/app/routes/index.vue ``` ``` ## `layouts`について[​](#layoutsについて "この見出しへの直接リンク") `layouts`は`app`層内に配置できます。そのためには、コンフィグを次のように変更します。 nuxt.config.ts ``` export default defineNuxtConfig({ devtools: { enabled: true }, // FSDには関係なく、プロジェクト起動時に有効 alias: { "@": '../src' }, dir: { pages: './src/app/routes', layouts: './src/app/layouts' } }) ``` ## 参照[​](#参照 "この見出しへの直接リンク") * [NuxtJSのディレクトリ設定変更に関するドキュメント](https://nuxt.com/docs/api/nuxt-config#dir) * [NuxtJSのルーター設定変更に関するドキュメント](https://nuxt.com/docs/guide/recipes/custom-routing#router-config) * [NuxtJSのエイリアス設定変更に関するドキュメント](https://nuxt.com/docs/api/nuxt-config#alias) --- # React Queryとの併用 ## キーをどこに置くか問題[​](#キーをどこに置くか問題 "この見出しへの直接リンク") ### 解決策 - エンティティごとに分割する[​](#解決策---エンティティごとに分割する "この見出しへの直接リンク") プロジェクトにすでにエンティティの分割があり、各クエリが1つのエンティティに対応している場合、エンティティごとに分割するのが最良です。この場合、次の構造を使用することをお勧めします。 ``` └── src/ # ├── app/ # | ... # ├── pages/ # | ... # ├── entities/ # | ├── {entity}/ # | ... └── api/ # | ├── `{entity}.query` # クエリファクトリー、キーと関数が定義されている | ├── `get-{entity}` # エンティティを取得する関数 | ├── `create-{entity}` # エンティティを作成する関数 | ├── `update-{entity}` # オブジェクトを更新する関数 | ├── `delete-{entity}` # オブジェクトを削除する関数 | ... # | # ├── features/ # | ... # ├── widgets/ # | ... # └── shared/ # ... # ``` もしエンティティ間に関係がある場合(例えば、「国」のエンティティに「都市」のエンティティ一覧フィールドがある場合)、`@x` アノテーションを使用した組織的なクロスインポートの[クロスインポート用のパブリックAPI](/documentation/ja/docs/reference/public-api.md#public-api-for-cross-imports)を利用するか、以下の代替案を検討できます。 ### 代替案 — クエリを公開で保存する[​](#代替案--クエリを公開で保存する "この見出しへの直接リンク") エンティティごとの分割が適さない場合、次の構造を考慮できます。 ``` └── src/ # ... # └── shared/ # ├── api/ # ... ├── `queries` # クエリファクトリー | ├── `document.ts` # | ├── `background-jobs.ts` # | ... # └── index.ts # ``` 次に、`@/shared/api/index.ts`に @/shared/api/index.ts ``` export { documentQueries } from "./queries/document"; ``` ## 問題「ミューテーションはどこに?」[​](#問題ミューテーションはどこに "この見出しへの直接リンク") ミューテーションをクエリと混合することは推奨されません。2つの選択肢が考えられます。 ### 1. 使用場所の近くにAPIセグメントにカスタムフックを定義する[​](#1-使用場所の近くにapiセグメントにカスタムフックを定義する "この見出しへの直接リンク") @/features/update-post/api/use-update-title.ts ``` export const useUpdateTitle = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ id, newTitle }) => apiClient .patch(`/posts/${id}`, { title: newTitle }) .then((data) => console.log(data)), onSuccess: (newPost) => { queryClient.setQueryData(postsQueries.ids(id), newPost); }, }); }; ``` ### 2. 別の場所(Shared層やEntities層)にミューテーション関数を定義し、コンポーネント内で`useMutation`を直接使用する[​](#2-別の場所shared層やentities層にミューテーション関数を定義しコンポーネント内でusemutationを直接使用する "この見出しへの直接リンク") ``` const { mutateAsync, isPending } = useMutation({ mutationFn: postApi.createPost, }); ``` @/pages/post-create/ui/post-create-page.tsx ``` export const CreatePost = () => { const { classes } = useStyles(); const [title, setTitle] = useState(""); const { mutate, isPending } = useMutation({ mutationFn: postApi.createPost, }); const handleChange = (e: ChangeEvent) => setTitle(e.target.value); const handleSubmit = (e: FormEvent) => { e.preventDefault(); mutate({ title, userId: DEFAULT_USER_ID }); }; return (
Create ); }; ``` ## クエリの組織化[​](#クエリの組織化 "この見出しへの直接リンク") ### クエリファクトリー[​](#クエリファクトリー "この見出しへの直接リンク") このガイドでは、クエリファクトリーの使い方について説明します。 注記 クエリファクトリーとは、JSオブジェクトのことで、そのオブジェクトキーの値がクエリキー一覧を返す関数である。 ``` const keyFactory = { all: () => ["entity"], lists: () => [...postQueries.all(), "list"], }; ``` 備考 `queryOptions` - react-query\@v5に組み込まれたユーティリティ(オプション) ``` queryOptions({ queryKey, ...options, }); ``` より高い型安全性と将来のreact-queryのバージョンとの互換性を確保し、クエリの関数やキーへのアクセスを簡素化するために、`@tanstack/react-query`の`queryOptions`関数を使用することができる[(詳細はこちら)](https://tkdodo.eu/blog/the-query-options-api#queryoptions)。 ### 1. クエリファクトリーの作成[​](#1-クエリファクトリーの作成 "この見出しへの直接リンク") @/entities/post/api/post.queries.ts ``` import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import { getPosts } from "./get-posts"; import { getDetailPost } from "./get-detail-post"; import { PostDetailQuery } from "./query/post.query"; export const postQueries = { all: () => ["posts"], lists: () => [...postQueries.all(), "list"], list: (page: number, limit: number) => queryOptions({ queryKey: [...postQueries.lists(), page, limit], queryFn: () => getPosts(page, limit), placeholderData: keepPreviousData, }), details: () => [...postQueries.all(), "detail"], detail: (query?: PostDetailQuery) => queryOptions({ queryKey: [...postQueries.details(), query?.id], queryFn: () => getDetailPost({ id: query?.id }), staleTime: 5000, }), }; ``` ### 2. アプリケーションコードでのクエリファクトリーの適用[​](#2-アプリケーションコードでのクエリファクトリーの適用 "この見出しへの直接リンク") ``` import { useParams } from "react-router-dom"; import { postApi } from "@/entities/post"; import { useQuery } from "@tanstack/react-query"; type Params = { postId: string; }; export const PostPage = () => { const { postId } = useParams(); const id = parseInt(postId || ""); const { data: post, error, isLoading, isError, } = useQuery(postApi.postQueries.detail({ id })); if (isLoading) { return
Loading...
; } if (isError || !post) { return <>{error?.message}; } return (

Post id: {post.id}

{post.title}

{post.body}

Owner: {post.userId}
); }; ``` ### クエリファクトリーを使用する利点[​](#クエリファクトリーを使用する利点 "この見出しへの直接リンク") * **クエリの構造化:** ファクトリーはすべてのAPIクエリを1か所に整理し、コードをより読みやすく、保守しやすくしている * **クエリとキーへの便利なアクセス:** ファクトリーはさまざまなタイプのクエリとそのキーへの便利なメソッドを提供している * **クエリの再フェッチ機能:** ファクトリーは、アプリケーションのさまざまな部分でクエリキーを変更することなく、簡単に再フェッチを行うことを可能にしている ## ページネーション[​](#ページネーション "この見出しへの直接リンク") このセクションでは、ページネーションを使用して投稿エンティティを取得するためのAPIクエリを行う`getPosts`関数の例を挙げます。 ### 1. `getPosts`関数の作成[​](#1-getposts関数の作成 "この見出しへの直接リンク") `getPosts`関数は、APIセグメント内の`get-posts.ts`ファイルにあります。 @/pages/post-feed/api/get-posts.ts ``` import { apiClient } from "@/shared/api/base"; import { PostWithPaginationDto } from "./dto/post-with-pagination.dto"; import { PostQuery } from "./query/post.query"; import { mapPost } from "./mapper/map-post"; import { PostWithPagination } from "../model/post-with-pagination"; const calculatePostPage = (totalCount: number, limit: number) => Math.floor(totalCount / limit); export const getPosts = async ( page: number, limit: number, ): Promise => { const skip = page * limit; const query: PostQuery = { skip, limit }; const result = await apiClient.get("/posts", query); return { posts: result.posts.map((post) => mapPost(post)), limit: result.limit, skip: result.skip, total: result.total, totalPages: calculatePostPage(result.total, limit), }; }; ``` ### 2. ページネーション用のクエリファクトリー[​](#2-ページネーション用のクエリファクトリー "この見出しへの直接リンク") `postQueries`クエリファクトリーは、投稿に関するさまざまなクエリオプションを定義し、事前に定義されたページとリミットを使用して投稿一覧を取得するクエリを含みます。 ``` import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import { getPosts } from "./get-posts"; export const postQueries = { all: () => ["posts"], lists: () => [...postQueries.all(), "list"], list: (page: number, limit: number) => queryOptions({ queryKey: [...postQueries.lists(), page, limit], queryFn: () => getPosts(page, limit), placeholderData: keepPreviousData, }), }; ``` ### 3. アプリケーションコードでの使用[​](#3-アプリケーションコードでの使用 "この見出しへの直接リンク") @/pages/home/ui/index.tsx ``` export const HomePage = () => { const itemsOnScreen = DEFAULT_ITEMS_ON_SCREEN; const [page, setPage] = usePageParam(DEFAULT_PAGE); const { data, isFetching, isLoading } = useQuery( postApi.postQueries.list(page, itemsOnScreen), ); return ( <> setPage(page)} page={page} count={data?.totalPages} variant="outlined" color="primary" /> ); }; ``` 注記 例は簡略化されている。 ## クエリ管理用の`QueryProvider`[​](#クエリ管理用のqueryprovider "この見出しへの直接リンク") このガイドでは、`QueryProvider`をどのように構成するべきかを説明します。 ### 1. `QueryProvider`の作成[​](#1-queryproviderの作成 "この見出しへの直接リンク") `query-provider.tsx`ファイルは`@/app/providers/query-provider.tsx`にあります。 @/app/providers/query-provider.tsx ``` import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactNode } from "react"; type Props = { children: ReactNode; client: QueryClient; }; export const QueryProvider = ({ client, children }: Props) => { return ( {children} ); }; ``` ### 2. `QueryClient`の作成[​](#2-queryclientの作成 "この見出しへの直接リンク") `QueryClient`はAPIクエリを管理するために使用されるインスタンスです。`query-client.ts`ファイルは`@/shared/api/query-client.ts`にあります。`QueryClient`はクエリキャッシング用の特定の設定で作成されます。 @/shared/api/query-client.ts ``` import { QueryClient } from "@tanstack/react-query"; export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, gcTime: 5 * 60 * 1000, }, }, }); ``` ## コード生成[​](#コード生成 "この見出しへの直接リンク") 自動コード生成のためのツールが存在しますが、これらは上記のように設定可能なものと比較して柔軟性が低いです。Swaggerファイルが適切に構造化されている場合、これらのツールの1つを使用して`@/shared/api`ディレクトリ内のすべてのコードを生成することができます。 ## RQの整理に関する追加のアドバイス[​](#rqの整理に関する追加のアドバイス "この見出しへの直接リンク") ### APIクライアント[​](#apiクライアント "この見出しへの直接リンク") 共有層であるshared層でカスタムのAPIクライアントクラスを使用することで、プロジェクト内でのAPI設定やAPI操作を標準化できます。これにより、ログ記録、ヘッダー、およびデータ交換形式(例: JSONやXML)を一元管理することができます。このアプローチにより、APIとの連携の変更や更新が簡単になり、プロジェクトのメンテナンスや開発が容易になります。 @/shared/api/api-client.ts ``` import { API_URL } from "@/shared/config"; export class ApiClient { private baseUrl: string; constructor(url: string) { this.baseUrl = url; } async handleResponse(response: Response): Promise { if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } try { return await response.json(); } catch (error) { throw new Error("Error parsing JSON response"); } } public async get( endpoint: string, queryParams?: Record, ): Promise { const url = new URL(endpoint, this.baseUrl); if (queryParams) { Object.entries(queryParams).forEach(([key, value]) => { url.searchParams.append(key, value.toString()); }); } const response = await fetch(url.toString(), { method: "GET", headers: { "Content-Type": "application/json", }, }); return this.handleResponse(response); } public async post>( endpoint: string, body: TData, ): Promise { const response = await fetch(`${this.baseUrl}${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), }); return this.handleResponse(response); } } export const apiClient = new ApiClient(API_URL); ``` ## 参照[​](#see-also "この見出しへの直接リンク") * [The Query Options API](https://tkdodo.eu/blog/the-query-options-api) --- # SvelteKitとの併用 SvelteKitプロジェクトでFSDを実装することは可能ですが、SvelteKitのプロジェクト構造要件とFSDの原則の違いにより、以下の2点でコンフリクトが発生してしまいます。 * SvelteKitは`src/routes`フォルダー内でファイル構造を作成することを提案しているが、FSDではルーティングは`app`層の一部である必要がある * SvelteKitは、ルーティングに関係のないすべてのものを`src/lib`フォルダーに入れることを提案している ## コンフィグファイルの設定[​](#コンフィグファイルの設定 "この見出しへの直接リンク") svelte.config.ts ``` import adapter from '@sveltejs/adapter-auto'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config}*/ const config = { preprocess: [vitePreprocess()], kit: { adapter: adapter(), files: { routes: 'src/app/routes', // ルーティングをapp層内に移動 lib: 'src', appTemplate: 'src/app/index.html', // アプリケーションのエントリーポイントをapp層内に移動 assets: 'public' }, alias: { '@/*': 'src/*' // srcディレクトリのエイリアスを作成 } } }; export default config; ``` ## `src/app`内へのファイルルーティングの移動[​](#srcapp内へのファイルルーティングの移動 "この見出しへの直接リンク") `app`層を作成し、アプリケーションのエントリーポイントである`index.html`を移動し、`routes`フォルダーを作成します。 最終的にファイル構造は次のようになります。 ``` ├── src │ ├── app │ │ ├── index.html │ │ ├── routes │ ├── pages # FSDに割り当てられたpagesフォルダー ``` これで、`app`内にページのルートを作成したり、`pages`からのページをルートに接続したりできます。 例えば、プロジェクトにホームページを追加するには、次の手順を実行します。 * `pages`層内にホームページスライスを追加する * `app`層の`routes`フォルダーに対応するルートを追加する * スライスのページとルートを統合する ホームページスライスを作成するには、[CLI](https://github.com/feature-sliced/cli)を使用します。 ``` fsd pages home ``` `ui`セグメント内に`home-page.svelte`ファイルを作成し、公開APIを介してアクセスできるようにします。  src/pages/home/index.ts ``` export { default as HomePage } from './ui/home-page.svelte'; ``` このページのルートを`app`層内に作成します。 ``` ├── src │ ├── app │ │ ├── routes │ │ │ ├── +page.svelte │ │ ├── index.html │ ├── pages │ │ ├── home │ │ │ ├── ui │ │ │ │ ├── home-page.svelte │ │ │ ├── index.ts ``` 最後に`+page.svelte`ファイル内にページコンポーネントを追加します。 src/app/routes/+page.svelte ``` ``` ## 参照[​](#参照 "この見出しへの直接リンク") * [SvelteKitのディレクトリ設定変更に関するドキュメント](https://kit.svelte.dev/docs/configuration#files) --- # Docs for LLMs This page provides links and guidance for LLM crawlers. * Spec: ### Files[​](#files "この見出しへの直接リンク") * [llms.txt](/documentation/ja/llms.txt) * [llms-full.txt](/documentation/ja/llms-full.txt) ### Notes[​](#notes "この見出しへの直接リンク") * Files are served from the site root, regardless of the current page path. * In deployments with a non-root base URL (e.g., `/documentation/`), the links above are automatically prefixed. --- # レイヤー レイヤーは、Feature-Sliced Designにおける組織階層の最初のレベルです。その目的は、コードを責任の程度やアプリ内の他のモジュールへの依存度に基づいて分離することです。各レイヤーは、コードにどれだけの責任を割り当てるべきかを判断するための特別な意味を持っています。 合計で**7つのレイヤー**があり、責任と依存度が最も高いものから最も低いものへと配置されています。 ![A file system tree, with a single root folder called src and then seven subfolders: app, processes, pages, widgets, features, entities, shared. The processes folder is slightly faded out.](/documentation/ja/img/layers/folders-graphic-light.svg#light-mode-only) ![A file system tree, with a single root folder called src and then seven subfolders: app, processes, pages, widgets, features, entities, shared. The processes folder is slightly faded out.](/documentation/ja/img/layers/folders-graphic-dark.svg#dark-mode-only) 1. App (アップ) 2. Processes (プロセス、非推奨) 3. Pages (ページ) 4. Widgets (ウィジェット) 5. Features (フィーチャー) 6. Entities (エンティティ) 7. Shared (シェアード) プロジェクトにすべてのレイヤーを使用する必要はありません。プロジェクトに価値をもたらすと思う場合のみ追加してください。通常、ほとんどのフロントエンドプロジェクトには、少なくともShared、Page、Appのレイヤーがあります。 実際には、レイヤーは小文字の名前のフォルダーです(例えば、`📁 shared`、`📁 pages`、`📁 app`)。新しいレイヤーを追加することは推奨されていません。なぜなら、その意味は標準化されているからです。 ## レイヤーに関するインポートルール[​](#レイヤーに関するインポートルール "この見出しへの直接リンク") レイヤーは *スライス* で構成されており、これは非常に凝集性の高いモジュールのグループです。スライス間の依存関係は、**レイヤーに関するインポートルール**によって規制されています。 > *スライス内のモジュール(ファイル)は、下位のレイヤーにある他のスライスのみをインポートできます。* 例えば、フォルダー `📁 ~/features/aaa` は「aaa」という名前のスライスです。その中のファイル `~/features/aaa/api/request.ts` は、`📁 ~/features/bbb` 内のファイルからコードをインポートすることはできませんが、`📁 ~/entities` や `📁 ~/shared` からコードをインポートすることができ、例えば `~/features/aaa/lib/cache.ts` などの同じスライス内の隣接コードもインポートできます。 AppとSharedのレイヤーは、このルールの**例外**です。これらは同時にレイヤーとスライスの両方です。スライスはビジネスドメインによってコードを分割しますが、これらの2つのレイヤーは例外です。なぜなら、Sharedはビジネスドメインを持たず、Appはすべてのビジネスドメインを統合しているからです。 実際には、AppとSharedのレイヤーはセグメントで構成されており、セグメントは自由に互いにインポートできます。 ## レイヤーの定義[​](#レイヤーの定義 "この見出しへの直接リンク") このセクションでは、各レイヤーの意味を説明し、どのようなコードがそこに属するかの直感を得るためのものです。 ### Shared[​](#shared "この見出しへの直接リンク") このレイヤーは、アプリの残りの部分の基盤を形成します。外部世界との接続を作成する場所であり、例えばバックエンド、サードパーティライブラリ、環境などです。また、特定のタスクに集中した自分自身のライブラリを作成する場所でもあります。 このレイヤーは、Appレイヤーと同様に、*スライスを含みません*。スライスはビジネスドメインによってレイヤーを分割することを目的としていますが、Sharedにはビジネスドメインが存在しないため、Shared内のすべてのファイルは互いに参照し、インポートすることができます。 このレイヤーで通常見られるセグメントは次のとおりです。 * `📁 api` — APIクライアントおよび特定のバックエンドエンドポイントへのリクエストを行う関数 * `📁 ui` — アプリケーションのUIキット
このレイヤーのコンポーネントはビジネスロジックを含むべきではありませんが、ビジネスに関連することは許可されています。例えば、会社のロゴやページレイアウトをここに置くことができます。UIロジックを持つコンポーネントも許可されています(例えば、オートコンプリートや検索バー) * `📁 lib` — 内部ライブラリのコレクション
このフォルダーはヘルパーやユーティリティとして扱うべきではありません([なぜこれらのフォルダーがしばしばダンプに変わるか](https://dev.to/sergeysova/why-utils-helpers-is-a-dump-45fo))。このフォルダー内の各ライブラリは、日付、色、テキスト操作など、1つの焦点を持つべきです。その焦点はREADMEファイルに文書化されるべきです。チームの開発者は、これらのライブラリに何を追加でき、何を追加できないかを知っているべきです * `📁 config` — 環境変数、グローバルフィーチャーフラグ、アプリの他のグローバル設定 * `📁 routes` — ルート定数、またはルートをマッチさせるためのパターン * `📁 i18n` — 翻訳のセットアップコード、グローバル翻訳文字列 さらにセグメントを追加することは自由ですが、これらのセグメントの名前は内容の目的を説明するものでなければなりません。例えば、`components`、`hooks`、`types` は、コードを探しているときにあまり役立たないため、悪いセグメント名です。 ### Entities[​](#entities "この見出しへの直接リンク") このレイヤーのスライスは、プロジェクトが扱う現実世界の概念を表します。一般的には、ビジネスがプロダクトを説明するために使用する用語です。例えば、SNSは、ユーザー、投稿、グループなどのビジネスエンティティを扱うかもしれません。 エンティティスライスには、データストレージ(`📁 model`)、データ検証スキーマ(`📁 model`)、エンティティ関連のAPIリクエスト関数(`📁 api`)、およびインターフェース内のこのエンティティの視覚的表現(`📁 ui`)が含まれる場合があります。視覚的表現は、完全なUIブロックを生成する必要はなく、アプリ内の複数のページで同じ外観を再利用することを主に目的としています。異なるビジネスロジックは、プロップスやスロットを通じてそれに付加されることがあります。 #### エンティティの関係[​](#エンティティの関係 "この見出しへの直接リンク") FSDにおけるエンティティはスライスであり、デフォルトではスライスは互いに知ることができません。しかし、現実の世界では、エンティティはしばしば互いに相互作用し、一方のエンティティが他のエンティティを所有、または含むことがあります。そのため、これらの相互作用のビジネスロジックは、フィーチャーやページのような上位のレイヤーに保持されるのが望ましいです。 一つのエンティティのデータオブジェクトが他のデータオブジェクトを含む場合、通常はエンティティ間の接続を明示的にし、`@x`表記を使用してスライスの隔離を回避するのが良いアイデアです。理由は、接続されたエンティティは一緒にリファクタリングする必要があるため、その接続を見逃すことができないようにするのが最善です。 例: entities/artist/model/artist.ts ``` import type { Song } from "entities/song/@x/artist"; export interface Artist { name: string; songs: Array; } ``` entities/song/@x/artist.ts ``` export type { Song } from "../model/song.ts"; ``` `@x`表記の詳細については、[クロスインポートの公開API](/documentation/ja/docs/reference/public-api.md#public-api-for-cross-imports)セクションを参照してください。 ### Features[​](#features "この見出しへの直接リンク") このレイヤーは、アプリ内の主要なインタラクション、つまりユーザーが行いたいことを対象としています。これらのインタラクションは、ビジネスエンティティを含むことが多いです。 アプリのフィーチャーレイヤーを効果的に使用するための重要な原則は、**すべてのものがフィーチャーである必要はない**ということです。何かがフィーチャーである必要がある良い指標は、それが複数のページで再利用されるという事実です。 例えば、アプリに複数のエディターがあり、すべてにコメントがある場合、コメントは再利用されるフィーチャーです。スライスはコードを迅速に見つけるためのメカニズムであり、フィーチャーが多すぎると重要なものが埋もれてしまいます。 理想的には、新しいプロジェクトに入ったとき、既存のページやフィーチャーを見ると、アプリの機能性が分かります。何がフィーチャーであるべきかを決定する際には、プロジェクトの新参者が重要なコードの大きな領域を迅速に発見できるように最適化してください。 フィーチャーのスライスには、インタラクションを実行するためのUI(例えばフォーム、`📁 ui`)、アクションを実行するために必要なAPI呼び出し(`📁 api`)、検証および内部状態(`📁 model`)、フィーチャーフラグ(`📁 config`)が含まれる場合があります。 ### Widgets[​](#widgets "この見出しへの直接リンク") ウィジェットレイヤーは、大きな自己完結型のUIブロックを対象としています。ウィジェットは、複数のページで再利用される場合や、所属するページにある複数の大きな独立したブロックの一つである場合に最も便利です。 UIのブロックがページの大部分を構成し、再利用されない場合、それは**ウィジェットであるべきではなく**、代わりにそのページ内に直接配置するべきです。 ヒント ネストされたルーティングシステム([Remix](https://remix.run)のルーターのような)を使用している場合、ウィジェットレイヤーを、フラットなルーティングシステムがページレイヤーを使用するのと同じように使用することが役立つかもしれません。それは関連データの取得、ローディング状態、エラーバウンダリを含む完全なルーターブロックを作成するためです。 同様に、このレイヤーにページレイアウトを保存することもできます。 ### Pages[​](#pages "この見出しへの直接リンク") ページは、ウェブサイトやアプリケーションを構成するものです(スクリーンやアクティビティとも呼ばれます)。通常、1ページは1つのスライスに対応しますが、非常に似たページが複数ある場合、それらを1つのスライスにまとめることができます。例えば、登録フォームとログインフォームです。 チームがナビゲートしやすい限り、ページスライスに配置できるコードの量に制限はありません。ページ上のUIブロックが再利用されない場合、それをページスライス内に保持することは完全に問題ありません。 ページスライスには、通常、ページのUIやローディング状態、エラーバウンダリ(`📁 ui`)、データの取得や変更リクエスト(`📁 api`)が含まれます。ページが専用のデータモデルを持つことは一般的ではなく、状態の小さな部分はコンポーネント自体に保持されることがあります。 ### Processes[​](#processes "この見出しへの直接リンク") 注意 このレイヤーは非推奨です。現在の仕様では、これを避け、その内容をFeaturesやAppに移動することを推奨しています。 プロセスは、マルチページインタラクションのための逃げ道です。 このレイヤーは意図的に未定義のままにされています。ほとんどのアプリケーションはこのレイヤーを使用せず、ルーターやサーバーレベルのロジックをAppレイヤーに保持するべきです。このレイヤーは、Appレイヤーが大きくなりすぎてメンテナンスが困難になった場合にのみ使用することを検討してください。 ### App[​](#app "この見出しへの直接リンク") アプリ全体に関するあらゆるもの、技術的な意味(例えば、コンテキストプロバイダー)やビジネス的な意味(例えば、分析)を含みます。 このレイヤーには通常、スライスは含まれず、Sharedと同様に、セグメントが直接存在します。 このレイヤーで通常見られるセグメントは次のとおりです。 * `📁 routes` — ルーターの設定 * `📁 store` — グローバルストアの設定 * `📁 styles` — グローバルスタイル * `📁 entrypoint` — アプリケーションコードへのエントリポイント、フレームワーク固有 --- # 公開API 公開APIは、モジュールのグループ(スライスなど)とそれを使用するコードとの間の**契約**です。また、特定のオブジェクトへのアクセスを制限し、その公開APIを通じてのみアクセスを許可します。 実際には、通常、再エクスポートを伴うインデックスファイルとして実装されます。 pages/auth/index.js ``` export { LoginPage } from "./ui/LoginPage"; export { RegisterPage } from "./ui/RegisterPage"; ``` ## 良い公開APIとは?[​](#良い公開apiとは "この見出しへの直接リンク") 良い公開APIは、他のコードとの統合を便利で信頼性の高いものにします。これを達成するためには、以下の3つの目標を設定することが重要です。 1. アプリケーションの残りの部分は、スライスの構造的変更(リファクタリングなど)から保護されるべきです。 2. スライスの動作における重要な変更が以前の期待を破る場合、公開APIに変更が必要です。 3. スライスの必要な部分のみを公開するべきです。 最後の目標には重要な実践的な意味があります。特にスライスの初期開発段階では、すべてをワイルドカードで再エクスポートしたくなるかもしれません。なぜなら、ファイルからエクスポートする新しいオブジェクトは、スライスからも自動的にエクスポートされるからです。 バッドプラクティス, features/comments/index.js ``` // ❌ これは悪いコードの例です。このようにしないでください。 export * from "./ui/Comment"; // 👎 export * from "./model/comments"; // 💩 ``` これは、スライスの理解可能性を損ないます。インターフェースが理解できないと、スライスのコードを深く掘り下げて統合方法を調べなければいけなくなってしまいます。もう一つの問題は、モジュールの内部を誤って公開してしまう可能性があり、誰かがそれに依存し始めるとリファクタリングが難しくなることです。 ## クロスインポートのための公開API[​](#public-api-for-cross-imports "この見出しへの直接リンク") クロスインポートは、同じレイヤーの別のスライスからインポートする状況です。通常、これは[レイヤーに関するインポートルール](/documentation/ja/docs/reference/layers.md#import-rule-on-layers)によって禁止されていますが、しばしば正当な理由でクロスインポートが必要です。たとえば、ビジネスエンティティは現実世界で互いに参照し合うことが多く、これらの関係をコードに反映させるのが最善です。 この目的のために、`@x`表記として知られる特別な種類の公開APIがあります。エンティティAとBがあり、エンティティBがエンティティAからインポートする必要がある場合、エンティティAはエンティティB専用の別の公開APIを宣言できます。 * `📂 entities` * `📂 A` * `📂 @x` * `📄 B.ts` — エンティティB内のコード専用の特別な公開API * `📄 index.ts` — 通常の公開API その後、`entities/B/`内のコードは`entities/A/@x/B`からインポートできます。 ``` import type { EntityA } from "entities/A/@x/B"; ``` `A/@x/B`という表記は「AとBが交差している」と読むことを意図しています。 注記 クロスインポートは最小限に抑え、**この表記はエンティティレイヤーでのみ使用してください**。クロスインポートを排除することがしばしば非現実的だからです。 ## インデックスファイルの問題[​](#インデックスファイルの問題 "この見出しへの直接リンク") `index.js`のようなインデックスファイル(Barrelファイルとも呼ばれる)は、公開APIを定義する最も一般的な方法です。作成は簡単ですが、特定のバンドラーやフレームワークで問題を引き起こすことがあります。 ### 循環インポート[​](#循環インポート "この見出しへの直接リンク") 循環インポートとは、2つ以上のファイルが互いに循環的にインポートすることです。 ![三つのファイルが循環的にインポートしている](/documentation/ja/img/circular-import-light.svg#light-mode-only)![三つのファイルが循環的にインポートしている](/documentation/ja/img/circular-import-dark.svg#dark-mode-only) 上の図には、`fileA.js`、`fileB.js`、`fileC.js`の三つのファイルが循環的にインポートしている様子が示されています。 これらの状況は、バンドラーにとって扱いが難しく、場合によってはデバッグが難しいランタイムエラーを引き起こすことさえあります。 循環インポートはインデックスファイルなしでも発生する可能性がありますが、インデックスファイルがあると、循環インポートを誤って作成する明確な機会が生まれます。これは、公開APIのスライスに2つのオブジェクト(例えば、`HomePage`と`loadUserStatistics`)が存在し、`HomePage`が`loadUserStatistics`に以下のようにアクセスすると循環インポートが発生してしまいます。 pages/home/ui/HomePage.jsx ``` import { loadUserStatistics } from "../"; // pages/home/index.jsからインポート export function HomePage() { /* … */ } ``` pages/home/index.js ``` export { HomePage } from "./ui/HomePage"; export { loadUserStatistics } from "./api/loadUserStatistics"; ``` この状況は循環インポートを作成します。なぜなら、`index.js`が`ui/HomePage.jsx`をインポートしますが、`ui/HomePage.jsx`が`index.js`をインポートするからです。 この問題を防ぐために、次の2つの原則を考慮してください。 * ファイルが同じスライス内にある場合は、常に**相対インポート**を使用し、完全なインポートパスを記述すること * ファイルが異なるスライスにある場合は、常に**絶対インポート**を使用すること(エイリアスなどで) ### Sharedにおける大きなバンドルサイズと壊れたツリーシェイキング[​](#large-bundles "この見出しへの直接リンク") インデックスファイルがすべてを再エクスポートする場合、いくつかのバンドラーはツリーシェイキング(インポートされていないコードを削除すること)に苦労するかもしれません。 通常、これは公開APIにとって問題ではありません。なぜなら、モジュールの内容は通常非常に密接に関連しているため、1つのものをインポートし、他のものをツリーシェイキングする必要がほとんどないからです。しかし、公開APIのルールが問題を引き起こす非常に一般的なケースが2つあります。それは`shared/ui`と`shared/lib`です。 これらの2つのフォルダーは、しばしば一度にすべてが必要ではない無関係なもののコレクションです。たとえば、`shared/ui`にはUIライブラリのすべてのコンポーネントのモジュールが含まれているかもしれません。 * `📂 shared/ui/` * `📁 button` * `📁 text-field` * `📁 carousel` * `📁 accordion` この問題は、これらのモジュールの1つが重い依存関係(シンタックスハイライトやドラッグ&ドロップライブラリ)を持っている場合、悪化します。ボタンなど、`shared/ui`から何かを使用するすべてのページにそれらを引き込むことは望ましくありません。 `shared/ui`や`shared/lib`の単一の公開APIによってバンドルサイズが不適切に増加する場合は、各コンポーネントやライブラリのために別々のインデックスファイルを持つことをお勧めします。 * `📂 shared/ui/` * `📂 button` * `📄 index.js` * `📂 text-field` * `📄 index.js` その後、これらのコンポーネントの消費者は、次のように直接インポートできます。 pages/sign-in/ui/SignInPage.jsx ``` import { Button } from '@/shared/ui/button'; import { TextField } from '@/shared/ui/text-field'; ``` ### 公開APIを回避することに対する実質的な保護がない[​](#公開apiを回避することに対する実質的な保護がない "この見出しへの直接リンク") スライスのためにインデックスファイルを作成しても、誰もそれを使用せず、直接インポートを使用することができます。これは特に自動インポートにおいて問題です。なぜなら、オブジェクトをインポートできる場所がいくつかあるため、IDEがあなたのために決定しなければならないからです。時には、直接インポートを選択し、スライスの公開APIルールを破ることがあります。 これらの問題を自動的にキャッチするために、[Steiger](https://github.com/feature-sliced/steiger)を使用することをお勧めします。これは、Feature-Sliced Designのルールセットを持つアーキテクチャリンターです。 ### 大規模プロジェクトにおけるバンドラーのパフォーマンスの低下[​](#大規模プロジェクトにおけるバンドラーのパフォーマンスの低下 "この見出しへの直接リンク") プロジェクト内に大量のインデックスファイルがあると、開発サーバーが遅くなる可能性があります。これは、TkDodoが[「Barrelファイルの使用をやめてください」](https://tkdodo.eu/blog/please-stop-using-barrel-files)という記事で指摘しています。 この問題に対処するためにできることはいくつかあります。 1. [「Sharedにおける大きなバンドルサイズと壊れたツリーシェイキング」](#large-bundles)のセクションと同じアドバイス — `shared/ui`や`shared/lib`の各コンポーネントやライブラリのために別々のインデックスファイルを持つこと。 2. スライスを持つレイヤーのセグメントにインデックスファイルを持たないようにすること。
たとえば、「コメント」フィーチャーのインデックス(`📄 features/comments/index.js`)がある場合、そのフィーチャーの`ui`セグメントのために別のインデックスを持つ理由はありません(`📄 features/comments/ui/index.js`)。 3. 非常に大きなプロジェクトがある場合、アプリケーションをいくつかの大きなチャンクに分割できる可能性が高いです。
たとえば、Google Docsは、ドキュメントエディターとファイルブラウザに非常に異なる責任を持っています。各パッケージが独自のレイヤーセットを持つ別々のFSDルートとしてモノレポを作成できます。いくつかのパッケージは、SharedとEntitiesレイヤーのみを持つことができ、他のパッケージはPagesとAppのみを持つことができ、他のパッケージは独自の小さなSharedを含むことができますが、他のパッケージからの大きなSharedも使用できます。 --- # スライスとセグメント ## スライス[​](#スライス "この見出しへの直接リンク") スライスは、Feature-Sliced Designの組織階層の第2レベルです。主な目的は、プロダクト、ビジネス、または単にアプリケーションにとっての意味に基づいてコードをグループ化することです。 スライスの名前は標準化されていません。なぜなら、それらはアプリケーションのビジネス領域によって直接決定されるからです。たとえば、フォトギャラリーには `photo`、`effects`、`gallery-page` というスライスがあるかもしれません。SNSには、`post`、`comments`、`news-feed` などの別のスライスが必要です。 Shared層とApp層にはスライスが含まれていません。これは、Sharedがビジネスロジックを含むべきではないため、プロダクト的な意味を持たないからです。また、Appはアプリケーション全体に関わるコードのみを含むべきであり、したがって分割は必要ありません。 ### 低結合と高凝集[​](#zero-coupling-high-cohesion "この見出しへの直接リンク") スライスは、独立した強く凝集しているコードファイルのグループとして設計されています。以下の図は、凝集(cohesion)と結合(coupling)といった複雑な概念を視覚化するのに役立ちます。 ![](/documentation/ja/img/coupling-cohesion-light.svg#light-mode-only)![](/documentation/ja/img/coupling-cohesion-dark.svg#dark-mode-only) この図は、 に触発されています。 理想的なスライスは、同じレベルの他のスライスから独立しており(低結合)、その主な目的に関連するコードの大部分を含んでいます(高凝集)。 スライスの独立性は、[層のインポートルール](/documentation/ja/docs/reference/layers.md#import-rule-on-layers)によって保証されます。 > *スライス内のモジュール(ファイル)は、厳密に下の層にあるスライスのみをインポートできます。* ### スライスの公開APIルール[​](#スライスの公開apiルール "この見出しへの直接リンク") スライス内では、コードは自由に整理できますが、スライスが質の高い公開APIを持っている限り、問題はありません。これが**スライスの公開APIのルール**の本質です。 > *各スライス(およびスライスを持たない層のセグメント)は、公開APIの定義を含む必要があります。* > > *あるスライス/セグメントの外部モジュールは、そのスライス/セグメントの内部ファイル構造ではなく、公開APIのみを参照できます。* 公開APIの要求の理由や、作成のベストプラクティスについては、[公開APIのガイド](/documentation/ja/docs/reference/public-api.md)を参照してください。 ### スライスのグループ[​](#スライスのグループ "この見出しへの直接リンク") 密接に関連するスライスは、フォルダに構造的にグループ化できますが、他のスライスと同じ隔離ルールを遵守する必要があります。グループ化用のフォルダ内での**コードの共有は許可されていません**。 ![「compose」、「like」、「delete」機能が「post」フォルダにグループ化されています。このフォルダには、禁止を示すために取り消し線が引かれた「some-shared-code.ts」ファイルもあります。](/documentation/ja/assets/images/graphic-nested-slices-b9c44e6cc55ecdbf3e50bf40a61e5a27.svg) ## セグメント[​](#セグメント "この見出しへの直接リンク") セグメントは、組織階層の第3および最後のレベルであり、その目的は、技術的な目的に基づいてコードをグループ化することです。 いくつかの標準化されたセグメント名があります。 * `ui` — UIに関連するすべてのもの:UIコンポーネント、日付フォーマッタ、スタイルなど。 * `api` — バックエンドとのインタラクション:リクエスト関数、データ型、マッパーなど。 * `model` — データモデル:スキーマ、インターフェース、ストレージ、ビジネスロジック。 * `lib` — スライス内のモジュールに必要なライブラリコード。 * `config` — 設定ファイルとフィーチャーフラグ。 [レイヤーに関するページ](/documentation/ja/docs/reference/layers.md#layer-definitions)には、これらのセグメントが異なる層でどのように使用されるかの例があります。 独自のセグメントを作成することもできます。カスタムセグメントの最も一般的な場所は、スライスが意味を持たないApp層とShared層です。 これらのセグメントの名前が、その内容が何のために必要かを説明するものであることを確認してください。たとえば、`components`、`hooks`、`types` は、コードを探すときにあまり役に立たないため、悪いセグメント名です。 --- ### 明確なビジネスロジック アーキテクチャはドメインモジュールで構成されているため、習得が容易である ---