This challenge restructured the routing from flat full-page views into a nested master-detail pattern. Here is a summary of every change and why it was made.
Before (Challenge 10):
/projects → ProjectListPage (full page: grid of cards)
/projects/new → NewProjectPage (full page: form)
/projects/:projectId → ProjectDetailPage (full page: detail)
After (Challenge 11):
/projects → ProjectsLayout (master-detail shell)
/projects (index) → ProjectEmptyState
/projects/:projectId → ProjectDetailPanel
/projects/:projectId (index) → TaskList
/projects/:projectId/tasks/:taskId → TaskDetail
/projects/new → NewProjectPage
Key insight: /projects/new must remain outside the ProjectsLayout route so it renders as a full page, not inside the master-detail shell. React Router matches routes in order; listing /projects/new before or alongside /projects/:projectId ensures the literal segment "new" is not parsed as a project ID.
A common beginner mistake is to store the selected project in useState:
// Anti-pattern: local state for selection
const [selectedId, setSelectedId] = useState<string | null>(null);This breaks browser navigation (Back/Forward buttons do not change the selection), prevents bookmarking, and makes deep-linking impossible.
The URL-driven approach:
// Correct: URL drives the selection
const { projectId } = useParams<{ projectId: string }>();
const selectedProject = INITIAL_PROJECTS.find(p => p.id === projectId);Every piece of selection state is encoded in the URL. The component re-renders when the URL changes, which React Router handles automatically.
React Router's <Outlet /> acts as a content slot. In this challenge there are three levels:
-
Layoutrenders the outer shell (Header + Sidebar + Footer). Its<Outlet />is whereProjectsLayoutorNewProjectPagerenders. -
ProjectsLayoutrenders the master-detail grid. Its<Outlet />(in the right panel) is whereProjectEmptyStateorProjectDetailPanelrenders. -
ProjectDetailPanelrenders the project's header info. Its<Outlet />is whereTaskListorTaskDetailrenders.
Each level only re-renders its own <Outlet /> slot when navigation happens — the surrounding structure stays mounted and preserves scroll position.
The master list (<aside className="master-list">) is an independent scroll container (overflow-y: auto). When the user selects project B after project A, only the right panel's <Outlet /> swaps content. The left panel stays mounted and its scroll position is unchanged.
This is why putting the two-column layout in a layout route (rather than rendering both list and detail inside one component with conditional rendering) matters for UX.
useParams reads from the closest matching route ancestor:
// Inside ProjectDetailPanel — reads :projectId
const { projectId } = useParams<{ projectId: string }>();
// Inside TaskDetail — reads both :projectId and :taskId
const { projectId, taskId } = useParams<{ projectId: string; taskId: string }>();Parameters defined in ancestor route paths are available in all descendant components via useParams.
ProjectListItem highlights itself when its project ID matches the current URL:
const { projectId } = useParams<{ projectId?: string }>();
const isSelected = projectId === id;This is equivalent to using NavLink's isActive prop but gives finer control over the rendered markup and avoids wrapping non-anchor elements in NavLink.
Alternatively:
<NavLink to={`/projects/${id}`} className={({ isActive }) => isActive ? 'selected' : ''}>Both approaches derive selection state from the URL, not from local state.
React Router's index attribute marks the default child when a parent route matches exactly:
<Route path="/projects" element={<ProjectsLayout />}>
<Route index element={<ProjectEmptyState />} /> {/* /projects */}
<Route path=":projectId" element={<ProjectDetailPanel />}>
<Route index element={<TaskList />} /> {/* /projects/proj-1 */}
<Route path="tasks/:taskId" element={<TaskDetail />} /> {/* /projects/proj-1/tasks/task-1 */}
</Route>
</Route>/projects→ProjectsLayoutrenders, its Outlet rendersProjectEmptyState/projects/proj-1→ProjectDetailPanelrenders, its Outlet rendersTaskList/projects/proj-1/tasks/task-1→ProjectDetailPanelrenders, its Outlet rendersTaskDetail
The current CSS uses a fixed 320px left panel with grid-template-columns: 320px 1fr. On narrow viewports this breaks. Production-quality approaches include:
- Use a media query to stack the panels vertically on mobile
- Hide the master list when a project is selected on mobile (navigate-only model)
- Use
minmax(240px, 320px)for the left column
These refinements are left as an exercise; the challenge focuses on the routing pattern, not responsive CSS.
- Adding tasks — no form for creating tasks; task data is hardcoded
- Shared project state — newly added projects still do not persist (introduced in Challenge 12 with Context)
- Loading/error states — data is synchronous; async data fetching comes in Challenge 14
- Optimistic UI — covered in a later challenge