diff --git a/i18n/en/docusaurus-plugin-content-docs/current/guides/issues/routes.mdx b/i18n/en/docusaurus-plugin-content-docs/current/guides/issues/routes.mdx index 9547356014..0231bc08d0 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/guides/issues/routes.mdx +++ b/i18n/en/docusaurus-plugin-content-docs/current/guides/issues/routes.mdx @@ -1,46 +1,438 @@ --- sidebar_position: 3 -sidebar_class_name: sidebar-item--wip +sidebar_label: Routing --- -import WIP from '@site/src/shared/ui/wip/tmpl.mdx' +# Routing -# Routing +This document explains how to decide where routing and redirects should be handled in Feature-Sliced Design. - +After reading this guide, you should be able to answer: -## Situation +- Which layer should handle URLs and routes? +- How much routing responsibility (if any) can be delegated to lower layers? +- How can you design routing so that changing the route structure requires minimal modifications? -Urls to pages are hardcoded in the layers below pages +--- + +## Core principles + +When dealing with routing in FSD, there are a few basic principles. + +UI flow logic such as URL, route, and redirect should be handled only in the `app` and `pages` layers. +Lower layers (`features`, `entities`, `widgets`) should be designed so they don’t need to know actual path strings. + +Path strings should be centralized in a single place such as `shared/config/routes`, and actual usage should be concentrated in `app` and `pages`. + +Domain logic and UI flow must be separated. +**“Login succeeded”** is a domain state/result (what happened), while **“navigate to the dashboard because it succeeded”** is UI flow. Do not handle both in the same place. + +For lower layers, pass only behavior via callbacks, props, or composition. +Path strings should be managed in one place via a configuration object like `ROUTES`. + +The strictness of these rules can vary depending on the team or project. +However, it’s generally best to avoid a structure where route strings are hard-coded across lower layers. + +--- + +## Layer responsibilities + +### `app` layer + +The `app` layer is the entry point of the application. +It initializes the global router and connects URLs to pages. + +What this layer knows is **which path matches which page**. +Decisions such as “where to send the user” based on domain conditions (login status/permissions) are usually handled in `pages`. + +```tsx title="app/router.tsx" +import { HomePage } from 'src/pages/home'; +import { ProfilePage } from 'src/pages/profile'; +import { ROUTES } from 'src/shared/config/routes'; + +export const routes = [ + { path: ROUTES.home, component: HomePage }, + { path: ROUTES.profile, component: ProfilePage }, +]; +``` + +This code only connects paths to page components. +Decisions like **“where should we redirect after login?”** are handled by each page. + +--- + +### `pages` layer + +The `pages` layer is responsible for screens that correspond to specific URLs. +It composes page components and handles page-level redirects. + +Domain state itself is managed by `features` and `entities`, and +`pages` simply decides where to navigate based on the result. + +For example, consider a login page: + +```tsx title="pages/login/ui.tsx" +import { LoginForm } from 'src/features/auth-by-email'; +import { ROUTES } from 'src/shared/config/routes'; +import { useRouter } from 'src/shared/lib/router'; + +export function LoginPage() { + const router = useRouter(); + + return ( + { + if (user.role === 'admin') router.push(ROUTES.admin); + else router.push(ROUTES.dashboard); + }} + /> + ); +} +``` + +`LoginForm` is responsible only for attempting and validating login. +The decision to navigate to `ROUTES.admin` for admins or `ROUTES.dashboard` for regular users is known only by `LoginPage`. + +--- + +### `widgets` layer + +The `widgets` layer composes multiple features and entities into reusable UI blocks. +Large reusable components used across pages, such as Header or Sidebar, belong here. + +Components in this layer only need to know that a button was clicked. +They should not know where to navigate or what the actual route is. + +Let’s use a Header as an example: + +```tsx title="widgets/header/ui.tsx" +type HeaderProps = { + onLogoClick: () => void; + onProfileClick: () => void; +}; + +export function Header({ onLogoClick, onProfileClick }: HeaderProps) { + return ( +
+ + +
+ ); +} +``` + +This Header only reports that the logo/profile button was clicked. +The `pages` layer decides where to navigate: + +```tsx title="pages/home/ui.tsx" +import { Header } from 'src/widgets/header'; +import { ROUTES } from 'src/shared/config/routes'; +import { useRouter } from 'src/shared/lib/router'; + +export function HomePage() { + const router = useRouter(); + + return ( + <> +
router.push(ROUTES.home)} + onProfileClick={() => router.push(ROUTES.profile)} + /> + {/* Page content */} + + ); +} +``` + +This way, Header doesn’t need to know any paths and can be reused even if the path structure changes. + +--- + +### `features` layer + +The `features` layer implements a single business action such as login, search, or creating an order. +This layer performs the action and is responsible for reporting success or failure. + +It’s best if this layer does not need to know where to navigate after success or what routes should be used. + +For example, a login form: + +```tsx title="features/auth-by-email/ui/login-form.tsx" +import { useState, type FormEvent } from 'react'; +import { loginUser } from '../api/login-user'; +import { useAuthStore, type User } from 'src/entities/user'; + +type LoginFormProps = { + onSuccess?: (user: User) => void; + onError?: (error: Error) => void; +}; + +export function LoginForm({ onSuccess, onError }: LoginFormProps) { + const { setUser } = useAuthStore(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + + try { + const user = await loginUser({ email, password }); + setUser(user); // Update domain state + onSuccess?.(user); // Notify success + pass required result + } catch (error) { + onError?.(error as Error); + } + }; + + return ( +
+ setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + +
+ ); +} +``` + +This form handles login attempts, validation, and error handling. +It only calls `onSuccess(user)` when login succeeds; the `pages` layer decides where to navigate. + +--- + +### `entities` layer + +The `entities` layer deals with domain models and state such as users, products, and orders. +It manages facts like **“the user is logged in”** or **“there are 3 items in the cart.”** + +Deciding where to redirect or which URL to navigate to is not the responsibility of this layer. + +```tsx title="entities/user/model/auth-store.ts" +import { create } from 'zustand'; -```tsx title="entities/post/card" +export type User = { + id: string; + email: string; + role: 'admin' | 'user'; + profileCompleted?: boolean; +}; - - void; + clearUser: () => void; +}; + +export const useAuthStore = create((set) => ({ + user: null, + setUser: (user) => set({ user }), + clearUser: () => set({ user: null }), +})); +``` + +--- + +### `shared` layer + +The `shared` layer contains technical utilities and common components reused across the project. +Code closer to environment/infrastructure—such as router adapters, history wrappers, and route configuration—lives here. + +For example, you can define a centralized `ROUTES` configuration: + +```tsx title="shared/config/routes.ts" +export const ROUTES = { + home: '/', + profile: '/my-account', + settings: '/settings', + dashboard: '/dashboard', + admin: '/admin', + onboarding: '/onboarding', +} as const; +``` + +You can also wrap the routing library so switching libraries later has minimal impact: + +```tsx title="shared/lib/router.ts" +export function useRouter() { + // Wrap the actual router library here + // If you change libraries later, only this file needs updates + return { + push: (path: string) => { /* ... */ }, + replace: (path: string) => { /* ... */ }, + back: () => { /* ... */ }, + }; +} +``` + +This kind of wrapper helps minimize changes across the codebase if the router library changes. + +--- + +## Domain logic vs UI flow + +Mixing domain logic and UI flow in the same code leads to problems. + +> Note: Imports are omitted for brevity. + +```tsx +// ❌ Anti-pattern: domain logic and UI flow are mixed +export function LoginManager() { + const router = useRouter(); + + const handleLogin = async (email: string, password: string) => { + const result = await fetch('/api/login', { + method: 'POST', + body: JSON.stringify({ email, password }), + }); + const userData = await result.json(); + + // Domain state update + navigation are mixed in one place + // ...update domain state... + router.push('/dashboard'); + }; + + return
{/* ... */}
; +} +``` + +As requirements grow, conditions keep piling up: + +```tsx +if (user.role === 'admin') router.push('/admin'); +else if (!user.profileCompleted) router.push('/onboarding'); +else router.push('/dashboard'); +``` + +From an FSD perspective, it’s better to separate responsibilities: + +- **The `feature` handles authentication/state update/success notification (passing only required result)**. +- **The `page` owns the redirect policy based on that result**. + +```tsx +import { LoginForm } from 'src/features/auth-by-email'; +import { ROUTES } from 'src/shared/config/routes'; +import { useRouter } from 'src/shared/lib/router'; + +export function LoginPage() { + const router = useRouter(); + + return ( + { + if (user.role === 'admin') router.push(ROUTES.admin); + else if (!user.profileCompleted) router.push(ROUTES.onboarding); + else router.push(ROUTES.dashboard); + }} /> - ... -
+ ); +} ``` -## Problem +--- -Urls are not concentrated in the page layer, where they belong according to the scope of responsibility +## Problems caused by scattered URLs -## If you ignore it +If path strings are used directly across multiple layers, maintenance becomes difficult. -Then, when changing urls, you will have to keep in mind that these urls (and the logic of urls/redirects) can be in all layers except pages +> Note: Imports are omitted for brevity. -And it also means that now even a simple product card takes part of the responsibility from the pages, which smears the logic of the project +```tsx +// ❌ Anti-pattern +export function Header() { + const router = useRouter(); + return ; +} +``` + +Now requirements change: you need to change the `profile` URL from `/profile` to `/my-account`. + +If strings are scattered, you must search and update them one by one, and missing one can cause a 404. + +To avoid this: + +1. Centralize paths in one place `(ROUTES)`. + +```tsx +export const ROUTES = { + home: '/', + profile: '/my-account', // e.g. only change here when requirements change + settings: '/settings', +} as const; +``` + +2. Use `ROUTES` in router configuration and pages. + +```tsx +import { ROUTES } from 'src/shared/config/routes'; + +const routes = [ + { path: ROUTES.profile, component: ProfilePage }, +]; +``` + +3. Pass only behavior via callbacks to lower layers. + +```tsx +type HeaderProps = { onProfileClick: () => void }; + +export function Header({ onProfileClick }: HeaderProps) { + return ; +} +``` + +```tsx +import { Header } from 'src/widgets/header'; +import { ROUTES } from 'src/shared/config/routes'; +import { useRouter } from 'src/shared/lib/router'; + +export function HomePage() { + const router = useRouter(); + return
router.push(ROUTES.profile)} />; +} +``` + +--- + +## Recommended patterns + +### Separate domain logic and UI flow + +A feature is responsible only for “what happened” (success/failure, state updates). +A page is responsible for “where to go next.” + +### Handle redirects in pages + +```tsx +// ✅ Recommended: feature passes result, page decides navigation + handleLoginSuccess(user)} /> + +// ❌ Avoid: feature calls router.push(...) internally +``` + +### Centralize path strings + +Centralize paths in a configuration object like `ROUTES`, and use them only in `app` and `pages`. + +### Use callbacks in widgets and features + +Lower layers do not perform navigation directly; they only report events such as click/complete to upper layers. + +--- -## Solution +## Rule strictness -Determine how to work with urls/redirects from the page level and above +Not every team needs to apply these rules with the same strictness. +However, it’s recommended to keep at least these two rules: -Transfer to the layers below via composition/props/factories +- Do not hard-code route strings in lower layers. +- Do not handle domain logic and redirect in the same function/component. -## See also +Even following just these two rules can significantly reduce costs when scaling the architecture or changing the URL structure. -- [(Thread) What if I "sew up" routing in entities/features/widgets](https://t.me/feature_sliced/4389) -- [(Thread) Why does it smear the logic of routes only in pages](https://t.me/feature_sliced/3756)