SummaryThis PR proposes a focused clarification to Feature‑Sliced Design (FSD): - DefineEntities asread‑only, pure UI/data views (same input → same output).
- DefineFeatures asall forms of write/interaction (domain writes, URL writes, client‑state writes, and side‑effects).
- Add a field‑testeddecision tree,rules of thumb,classification tables, andcommon pitfalls to help teams keep boundaries intact.
- Provide an optional, stack‑agnosticServer‑first appendix (with Next.js/RSC as one concrete mapping).
The proposaldoes not change the layer model, slice/segment rules, or public API rules. It tightens semantics that already exist in FSD and offers pragmatic guardrails that reflect real‑world usage. See FSD layer and slice/segment references for context. ([feature-sliced.design]1)
MotivationTeams frequently blur boundaries by: - Adding
onClick or router hooks toEntities. - ComputingURLs inside lower layers.
- Mixing display and interaction, which harms caching, bundle size, and testability.
While FSD defineslayers (app → … → entities → shared) andsegments (ui,api,model, etc.), the docs stop short of a crisp, testableboundary heuristic for Entities vs Features. This PR supplies that heuristic and maps it to modern Server‑first rendering where appropriate. ([feature-sliced.design]1) Additionally, the current “Routing” issue page (WIP) warns about URL logic leaking into lower layers; this PR contributes concrete guidance and examples to operationalize that page. ([feature-sliced.design]2)
Scope of change (documentation only)No normative changes to import rules, slices/segments, or public API. Cross‑imports and@x remain as documented. ([feature-sliced.design]3)
Proposed content (draft)1) DefinitionsEntities (Read) - Purpose:Pure, reusable rendering of business concepts.
- Behavior: Given the same input, returns the same markup (pure view).
- No local interaction handlers, router mutations, side‑effects, timers, or implicit state.
- May renderlinks given via props (href computed elsewhere).
- Typical segments:
ui for view,model for view types/schemas,api forread/SELECT helpers if needed. ([feature-sliced.design]1)
Features (Write) Purpose:All user interactions and writes. Categories: - Domain write (server/data): create/update/delete (e.g., “Quiz”, “User”).
- State write (client/URL): infinite scroll, filter, sort, search, pagination, selection, clipboard, downloads, prefetch, sockets, etc.
Ownshandlers (on*),URL/query changes,side‑effects,optimistic UX,permissions for actions, andtesting of interactions. Typical segments:ui (forms/buttons/observers),model (local state/validators),api (mutations). ([feature-sliced.design]1)
Mapping note (unchanged FSD semantics): Features = “main interactions users care about”, Entities = “business concepts.” This PR sharpens that distinction usingRead vs Write as a practical boundary that teams can test/enforce. ([feature-sliced.design]1)
2) Rules of thumb (operational)- If you see an
on* handler, it belongs inFeatures. - If youtouch the router/URL, it’sFeatures (URL is state; changing it is a write).
- Links are allowed in Entitiesonly if thehref is provided by upper layers (Pages/Features) and doesnot compute or mutate view conditions.
- When unsure, decide by“Who owns the action?” and“Same input → same output?” If it writes (domain/URL/client‑state/side‑effects) →Features.
Routing addendum: avoid hard‑coding URLs in lower layers; pass them down from Pages/Features via props/builders. ([feature-sliced.design]2)
3) Decision treeDoes this component trigger any write? (DB, URL, client state, side‑effects like clipboard, prefetch, socket) →Yes:Features. →No: go to 2. Is it pure? (Same input ⇒ same markup.) →Yes:Entities. →No: likelyFeatures decides conditions and injects props to an Entity view. Is it a link? - Resource navigation (no condition write):Entities can render the linkgiven an href prop.
- View/condition change (filters, panel/modals via query):Features builds and owns the href.
4) Server‑first appendix (optional, stack‑agnostic; Next.js as example)Minimal folder sketch (fits existing FSD layers/segments): src/├─ pages|app/ … # routing/composition├─ entities/│ └─ <domain>/<read-view>/│ ├─ ui/*.tsx # pure views (server-first)│ └─ action/get*.ts # SELECT-only helpers (optional)├─ features/│ └─ <feature>/<intent>/│ ├─ ui.tsx # client component (handlers)│ ├─ model/state.ts # client state (optional)│ └─ action/*.ts # server actions (POST/PUT/DELETE)├─ widgets/├─ shared/ (ui|lib|api|config|routes|i18n) (Still honors layers, segments, public API, and import rules.) ([feature-sliced.design]1)
5) Classification cheat‑sheet (abridged)- Entities: resource details/card/table/chartdisplay, static breadcrumbs/nav, static SEO/OG output, audit logview, loading skeletons/empty states (pure), resource links (href passed in).
- Features: filter/sort/search/pagination/infinite scroll, create/update/delete, optimistic updates, drafts/autosave, file upload/download (generated), selection/bulk actions, tooltips with state, sockets/SSE/presence, prefetch on hover/in‑view, route guards/redirects, A/B switches & analytics, geolocation & permissions, PWA prompts, query‑based modals/panels, payment/queues, toasts & retry, error boundaries with recovery.
6) Testing guidance (brief)- Entities → snapshot + a11y; contract tests for props.
- Features → interaction/E2E (happy path, rollback, routing, side‑effects).
7) Common pitfalls (and fixes)- Router/URL in Entities (
useSearchParams,router.*) →move to Features, pass props/href down. - “Just one onClick” in Entity → becomes a write; move to Feature.
- Hardcoded URLs in lower layers → push up to Pages/Features; supply href via props/builders. ([feature-sliced.design]2)
- Wildcard public APIs or cross‑imports without
@x → follow Public API rules and@x notation. Consider Steiger for enforcement. ([feature-sliced.design]3)
Backward compatibility- No breaking changes.
- Purely documentary; aligns with existing layers, slices/segments, public API, and routing guidance. ([feature-sliced.design]1)
Alternatives considered
Open questions for maintainers- Where should theURL‑write rule live long‑term — inRouting (Guides/Issues) orEntities/Features references (cross‑linked)? ([feature-sliced.design]2)
- Should we include a shortlinters/tooling sidebar (e.g., Steiger rule examples) in Public API/import rules? ([feature-sliced.design]3)
- Would you accept aframework‑appendix pattern (Next.js first, then Vue/Nuxt, SvelteKit, Angular) to keep the core spec stack‑agnostic?
Checklist
Appendix: References (for reviewers)
If maintainers agree with the direction, I can follow up by splitting this into: - a new guide page (Read vs Write boundary),
- a short Entities/Features reference edit, and
- a Routing page PR with examples.
|