294 lines
10 KiB
Markdown
294 lines
10 KiB
Markdown
# Migration Guide
|
|
|
|
How to migrate to FSD v2.1 from either FSD v2.0 or a custom (non-FSD)
|
|
architecture. This guide reflects the official `from-custom` step order:
|
|
**pages first**, then everything else.
|
|
|
|
## Part 1: FSD v2.0 → v2.1 (non-breaking)
|
|
|
|
The v2.1 update emphasizes **"pages first"**: most logic stays in pages,
|
|
reusable foundation in Shared. If reuse is needed across several pages,
|
|
move it to a layer below. The migration is non-breaking and simplifies
|
|
the codebase by relocating single-use code back to where it is consumed.
|
|
|
|
Another addition in v2.1 is the standardization of cross-imports between
|
|
entities with the `@x` notation. See `references/cross-import-patterns.md`.
|
|
|
|
### Step 1. Audit existing slices
|
|
|
|
Use Steiger to detect slices that are used in only one place:
|
|
|
|
```bash
|
|
npm install -D @feature-sliced/steiger
|
|
npx steiger src
|
|
```
|
|
|
|
Look for these rules:
|
|
|
|
- **`insignificant-slice`**: an entity or feature used by only one page.
|
|
This rule will suggest merging that entity or feature into the page
|
|
entirely.
|
|
- **`excessive-slicing`**: too many slices in a single layer.
|
|
|
|
For each flagged slice, decide:
|
|
|
|
- Reused in 2+ places → keep in features/entities.
|
|
- Used only in one page → mark for migration back into that page.
|
|
|
|
### Step 2. Move single-use code back to its consumer
|
|
|
|
Take single-use features and entities and inline them into the consuming
|
|
page (or widget if that is the single consumer):
|
|
|
|
```text
|
|
// Before (v2.0): feature used by only one page
|
|
features/user-profile-form/
|
|
ui/ProfileForm.tsx
|
|
model/profile-form.ts
|
|
api/update-profile.ts
|
|
index.ts
|
|
pages/profile/
|
|
ui/ProfilePage.tsx ← Thin wrapper, just composes
|
|
|
|
// After (v2.1): code lives in the page that owns it
|
|
pages/profile/
|
|
ui/{ProfilePage,ProfileForm}.tsx
|
|
model/profile.ts ← Merged form logic
|
|
api/update-profile.ts
|
|
index.ts
|
|
```
|
|
|
|
For each moved slice:
|
|
|
|
1. Copy all files into the consuming page.
|
|
2. Update the page's `index.ts` to export what is needed externally.
|
|
3. Update all imports across the codebase to point to the new location.
|
|
4. Delete the now-empty feature/entity directory.
|
|
5. Run tests.
|
|
|
|
### Step 3. Keep genuinely reused code in place
|
|
|
|
Code confirmed to be used in 2+ places stays in features/entities. Do not
|
|
move it. The point of v2.1 is reducing premature extraction, not removing
|
|
reuse.
|
|
|
|
### Step 4. Deprecate the processes layer
|
|
|
|
The `processes` layer is deprecated. Migrate its code:
|
|
|
|
- **Multi-page workflows** (checkout, onboarding wizard): move
|
|
orchestration logic to the page that initiates the workflow. If multiple
|
|
pages share workflow state, create a feature for it.
|
|
- **Background processes** (polling, sync): move to `app/` if global, or
|
|
to the relevant page/feature if scoped.
|
|
|
|
```text
|
|
// Before
|
|
processes/
|
|
checkout/model/checkout-flow.ts
|
|
sync/model/background-sync.ts
|
|
|
|
// After
|
|
features/checkout/model/checkout-flow.ts ← Used in 2+ pages
|
|
app/sync/background-sync.ts ← Global concern
|
|
```
|
|
|
|
### Post-migration verification
|
|
|
|
1. Run `npx steiger src`. All `insignificant-slice` warnings should be gone.
|
|
2. Verify import directions. No upward or same-layer cross-imports.
|
|
3. Check that no empty layer directories remain.
|
|
4. Update documentation to reflect the new structure.
|
|
|
|
## Part 2: Custom architecture → FSD
|
|
|
|
This part follows the official `from-custom` migration order. The core
|
|
philosophy is **pages first**: start by dividing the code by pages, then
|
|
work outward.
|
|
|
|
### Before you start
|
|
|
|
The most important question to ask the team is: *do you really need it?*
|
|
Some projects are perfectly fine without FSD. Reasons to consider the
|
|
switch:
|
|
|
|
1. New team members struggle to reach a productive level.
|
|
2. Modifications to one part of the code **often** break unrelated parts.
|
|
3. Adding new functionality is difficult due to the volume of context to
|
|
hold in mind.
|
|
|
|
**Avoid switching to FSD against the will of teammates**, even as a lead.
|
|
Convince the team that the benefits outweigh migration and learning costs.
|
|
Explain the migration plan to management; architectural changes are not
|
|
immediately observable to them.
|
|
|
|
If the decision is made, set up a path alias for `src/` first. This guide
|
|
uses `@` as an alias for `./src`.
|
|
|
|
### Step 1. Divide the code by pages
|
|
|
|
If `pages/` already exists, skip this step. Otherwise, create `pages/` and
|
|
move as much component code as possible from `routes/` (or equivalent) into
|
|
it. Aim for tiny route files that just re-export from page slices.
|
|
|
|
```text
|
|
// Route file (thin)
|
|
src/routes/products.[id].js
|
|
export { ProductPage as default } from "@/pages/product"
|
|
|
|
// Page slice
|
|
src/pages/product/
|
|
ui/ProductPage.jsx
|
|
index.js ← export { ProductPage } from "./ProductPage.jsx"
|
|
```
|
|
|
|
Pages may reference each other for now. Tackle that later. Focus on
|
|
establishing a prominent division by pages.
|
|
|
|
### Step 2. Separate everything else from pages
|
|
|
|
Create `src/shared/` and move everything that does **not** import from
|
|
`pages/` or `routes/` there. Create `src/app/` and move everything that
|
|
**does** import the pages or routes there, including the routes themselves.
|
|
|
|
The Shared layer has no slices, so segments may import from each other.
|
|
|
|
```text
|
|
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/
|
|
```
|
|
|
|
### Step 3. Tackle cross-imports between pages
|
|
|
|
Find all cases where one page imports from another. Resolve each in one of
|
|
two ways:
|
|
|
|
1. **Copy-paste** the imported code into the depending page to remove the
|
|
dependency.
|
|
2. **Move to a Shared segment**:
|
|
- UI kit code → `shared/ui/`
|
|
- configuration constants → `shared/config/`
|
|
- backend interaction → `shared/api/`
|
|
|
|
Copy-pasting is **not architecturally wrong**. Sometimes it is more correct
|
|
to duplicate than to abstract into a new reusable module, because the
|
|
shared parts of pages can drift apart over time. Still, the DRY principle
|
|
holds for business logic: avoid copy-pasting code that must stay in sync
|
|
across multiple places.
|
|
|
|
### Step 4. Unpack the Shared layer
|
|
|
|
The Shared layer can become bloated after Step 2. Find every object used in
|
|
only one page and move it to that page's slice. **This applies to actions,
|
|
reducers, and selectors too.** There is no benefit in grouping all actions
|
|
together, but there is benefit in colocating relevant actions close to
|
|
their usage.
|
|
|
|
```text
|
|
src/
|
|
pages/
|
|
product/
|
|
actions/, reducers/, selectors/, ui/ ← moved from shared
|
|
index.js
|
|
catalog/
|
|
shared/ ← only objects that are reused
|
|
actions/, api/, components/, ...
|
|
```
|
|
|
|
### Step 5. Organize code by technical purpose (segments)
|
|
|
|
In FSD, division by technical purpose is done with **segments**. The common
|
|
ones are:
|
|
|
|
- **`ui`**: everything related to UI display (components, date formatters,
|
|
styles).
|
|
- **`api`**: backend interactions (request functions, data types, mappers).
|
|
- **`model`**: the data model (schemas, interfaces, stores, business
|
|
logic).
|
|
- **`lib`**: library code that other modules in the slice need.
|
|
- **`config`**: configuration files and feature flags.
|
|
|
|
Custom segments are allowed when needed. **Do not create segments that
|
|
group code by what it is**, like `components`, `actions`, `types`, or
|
|
`utils`. Group code by what it is **for**, not by what it is. This is the
|
|
desegmentation principle.
|
|
|
|
Reorganize each page to separate code by segments:
|
|
|
|
- The existing page UI files become the `ui` segment.
|
|
- Actions, reducers, and selectors become the `model` segment.
|
|
- Thunks and mutations become the `api` segment.
|
|
|
|
Reorganize the Shared layer too:
|
|
|
|
- `components/`, `containers/` → most of it becomes `shared/ui/`.
|
|
- `helpers/`, `utils/` → group by function (dates, type conversions, etc.)
|
|
and move groups to `shared/lib/`.
|
|
- `constants/` → group by function and move to `shared/config/`.
|
|
|
|
## Optional steps
|
|
|
|
### Step 6. Form entities/features from Redux slices used on several pages
|
|
|
|
Reused Redux slices typically describe business concepts (products, users)
|
|
or user actions (comments, likes):
|
|
|
|
- Business entities → **Entities layer**, one entity per folder.
|
|
- User actions → **Features layer**.
|
|
|
|
Entities and features are meant to be independent. If your business domain
|
|
contains inherent connections between entities (a song belongs to an
|
|
artist), see the
|
|
[business entities cross-references guide](https://fsd.how/docs/guides/examples/types#business-entities-and-their-cross-references).
|
|
|
|
API functions related to these slices can stay in `shared/api`.
|
|
|
|
### Step 7. Refactor your modules
|
|
|
|
The `modules/` folder typically holds business logic, similar in nature to
|
|
the Features layer. Some modules describe large UI chunks (an app header)
|
|
which belong in the Widgets layer.
|
|
|
|
### Step 8. Form a clean UI foundation in `shared/ui`
|
|
|
|
`shared/ui` should contain UI elements with no encoded business logic.
|
|
Refactor components from `components/` and `containers/` to extract their
|
|
business logic to higher layers. If business logic is not used in many
|
|
places, copy-pasting back to consumers is an acceptable choice.
|
|
|
|
## Common pitfalls during migration
|
|
|
|
1. **Extracting too early.** Wait for real reuse, not anticipated reuse.
|
|
The v2.1 philosophy is "pages first, extract later".
|
|
2. **Creating empty layers.** Do not create `features/`, `entities/`, or
|
|
`widgets/` directories until there is content for them.
|
|
3. **Refactoring while migrating.** Separate relocation from refactoring.
|
|
Move files first, improve them in separate commits.
|
|
4. **Ignoring import direction.** Enforce import rules from day one with
|
|
ESLint or Steiger.
|
|
5. **Big-bang migration.** Migrate page by page, verifying each step. A
|
|
hybrid structure (partly FSD, partly legacy) is acceptable during
|
|
transition.
|
|
6. **Grouping by technical role.** `components/`, `actions/`, `utils/` as
|
|
segment names defeat the purpose of FSD. Group by what code is for.
|
|
|
|
## Migrating from FSD v1 to v2
|
|
|
|
This guide does not cover v1 → v2. See the official
|
|
[v1 to v2 migration guide](https://fsd.how/docs/guides/migration/from-v1).
|
|
The v1 → v2 transition introduced the entities and processes layers
|
|
(processes was later deprecated in v2.1).
|