In the previous challenge you used TanStack Query for all server state and React context for auth/theme. One gap remained: UI state that needs to survive navigation — sidebar collapse, filter preferences, and recently-viewed history.
In this challenge you will add Zustand as a lightweight global client-state store, giving that UI state a proper home without the boilerplate of Context + useReducer.
- Create a typed Zustand store with multiple state slices
- Write selector hooks to subscribe only to the data a component needs
- Use
shallowcomparison to prevent unnecessary re-renders when selecting objects - Understand the module-level singleton pattern Zustand uses
- Distinguish when to use Zustand vs. TanStack Query vs. React Context
Create a Zustand store with four slices:
| Slice | State | Actions |
|---|---|---|
| Sidebar | sidebarCollapsed: boolean |
toggleSidebar() |
| Filters | statusFilter, sortOrder |
setStatusFilter(), setSortOrder() |
| Recently Viewed | recentlyViewed: string[] (last 5 project IDs) |
addRecentlyViewed(id) |
| Notifications | showSuccessToasts, showErrorToasts |
toggleNotificationPref(key) |
The recentlyViewed list must:
- Deduplicate (moving an existing ID to the front)
- Cap at 5 entries
Export four named hooks. Each must use shallow comparison when returning an object:
export const useSidebarState = () => useAppStore((s) => ..., shallow)
export const useFilterPreferences = () => useAppStore((s) => ..., shallow)
export const useRecentlyViewed = () => useAppStore((s) => s.recentlyViewed)
export const useNotificationPrefs = () => useAppStore((s) => ..., shallow)Replace the local useState for sidebar collapse with useSidebarState().
Add a sidebar toggle button somewhere in the header or layout that calls toggle.
When isCollapsed is true, apply a CSS class (app-sidebar--collapsed) to the aside element so it narrows.
- Accept an
isCollapsedprop - When collapsed, hide link labels (show only icons or a narrow strip)
Replace the local useState (or useProjectFilters hook) with useFilterPreferences() from the store.
The filter and sort selection must survive navigation away and back.
Call addRecentlyViewed(projectId) inside a useEffect whenever projectId changes and the project data is loaded.
Add a small "Settings" popover or inline controls that let the user toggle showSuccessToasts and showErrorToasts.
You do not need to gate toasts behind these flags in
TaskItem— just expose the controls so the state is usable.
- Import
shallowfrom'zustand/shallow' create<AppState>((set) => ({ ... }))— the type parameter makes everything strict- You do not need
persistmiddleware for this challenge (that is the debrief topic) - The store is a singleton at module scope — no Provider needed
src/
store/
useAppStore.ts ← NEW: Zustand store
selectors.ts ← NEW: selector hooks
components/
Layout.tsx ← updated: useSidebarState
Sidebar.tsx ← updated: isCollapsed prop
Header.tsx ← updated: notification prefs
pages/
ProjectsLayout.tsx ← updated: useFilterPreferences
ProjectDetailPanel.tsx ← updated: addRecentlyViewed
npm install
npx msw init public/ --save
npm run devThen open http://localhost:5173.