User Progress Tracking and Persistence in developer-roadmap
The developer-roadmap application implements user progress tracking and persistence using Nanostores for reactive client-side state, React Query for server-state synchronization, and a JWT-protected REST API—contrary to common assumptions, no Zustand stores are present in the codebase.
The kamranahmedse/developer-roadmap repository powers roadmap.sh, an interactive platform for developer learning paths. While many React applications adopt Zustand for state management, this codebase specifically leverages Nanostores (tiny atomic reactive stores) combined with TanStack Query (React Query) to track topic completion states across roadmaps and best-practice guides, persisting data via dedicated API endpoints.
Architecture Overview: Nanostores Instead of Zustand
The application deliberately avoids Zustand in favor of a more modular, lightweight architecture. State management is split between two complementary systems:
- Nanostores (
src/stores/roadmap.ts) handle ephemeral, reactive UI state such as the current progress arrays and total node counts. - React Query (
src/queries/resource-progress.ts) manages server-state synchronization, background caching, and query invalidation. - REST API endpoints (
/v1-get-user-resource-progressand/v1-update-resource-progress) provide the durable persistence layer backed by a database.
This separation allows the UI to react instantly to local changes while ensuring eventual consistency with the backend through standardized HTTP methods.
The Seven-Step Progress Lifecycle
When a user interacts with a roadmap topic, the system follows a precise flow from identification to persistence:
-
Resource Identification: The UI identifies the context using
resourceType(roadmaporbest-practice),resourceId, andtopicIdvia theTopicMetatype insrc/lib/resource-progress.ts. -
Authentication Check: The system verifies the presence of a JWT authentication cookie (
TOKEN_COOKIE_NAME) usingCookies.get. If absent, progress operations return empty defaults and disable persistence. -
Initial Data Fetch: For authenticated users,
getResourceProgressexecutes a GET request to/v1-get-user-resource-progress, returning arrays of topic IDs categorized asdone,learning,skipped, and apersonalizedobject. -
Store Hydration: The response populates two Nanostores defined in
src/stores/roadmap.ts:roadmapProgress– Contains the four state arraystotalRoadmapNodes– Tracks the total topic count for percentage calculations
-
UI Rendering: Components consuming these stores via
useStorefrom@nanostores/reactautomatically re-render. Visual states are applied through CSS classes (done,learning,skipped,removed) manipulated byrenderTopicProgress. -
Progress Updates: When users toggle topics,
updateResourceProgresssends a POST request to/v1-update-resource-progresswith the new state. -
Cache Synchronization: After a successful update, the function updates both the Nanostores and the React Query cache using
queryClient.setQueryData, ensuring all components reflect the change immediately without refetching.
Fetching Progress from the Backend
The getResourceProgress function in src/lib/resource-progress.ts serves as the entry point for loading a user's history. It handles authentication verification, API communication, and store initialization in a single async flow.
// src/lib/resource-progress.ts
export async function getResourceProgress(
resourceType: 'roadmap' | 'best-practice',
resourceId: string,
) {
// Bail out if the user isn't logged in
if (!Cookies.get(TOKEN_COOKIE_NAME)) {
return { done: [], learning: [], skipped: [], personalized: { topicIds: [], information: '' } };
}
const { response, error } = await httpGet<{
done: string[];
learning: string[];
skipped: string[];
isFavorite: boolean;
personalized: { topicIds: string[]; information: string };
}>(`${import.meta.env.PUBLIC_API_URL}/v1-get-user-resource-progress`, {
resourceType,
resourceId,
});
if (error || !response) return { done: [], learning: [], skipped: [], personalized: { topicIds: [], information: '' } };
// Update the Nanostores so the UI reacts automatically
roadmapProgress.set({
done: response.done,
learning: response.learning,
skipped: response.skipped,
personalized: response.personalized,
});
// Notify any listeners that the favourite flag changed
window.dispatchEvent(new CustomEvent('mark-favorite', { detail: { resourceType, resourceId, isFavorite: response.isFavorite } }));
return response;
}
Persisting User Changes
When users mark topics as complete or in-progress, updateResourceProgress implements an optimistic update pattern. It transmits changes to the server, then synchronizes both the Nanostores and React Query cache to maintain UI consistency.
// src/lib/resource-progress.ts
export async function updateResourceProgress(
topic: TopicMeta,
progressType: ResourceProgressType,
) {
const { topicId, resourceType, resourceId } = topic;
const { response, error } = await httpPost<{
done: string[];
learning: string[];
skipped: string[];
isFavorite: boolean;
personalized: { topicIds: string[]; information: string };
}>(`${import.meta.env.PUBLIC_API_URL}/v1-update-resource-progress`, {
topicId,
resourceType,
resourceId,
progress: progressType,
});
if (error || !response?.done || !response?.learning) {
throw new Error(error?.message || 'Something went wrong');
}
// Nanostore – UI updates instantly
roadmapProgress.set({
done: response.done,
learning: response.learning,
skipped: response.skipped,
personalized: response.personalized,
});
// Keep React-Query in sync
queryClient.setQueryData(
userResourceProgressOptions(resourceType, resourceId).queryKey,
(oldData) => ({ ...(oldData || {}), done: response.done, learning: response.learning, skipped: response.skipped })
);
return response;
}
Reactive Store Definitions
The Nanostores are defined atomically in src/stores/roadmap.ts. These exports provide the reactive primitives that components subscribe to using the @nanostores/react hook.
// src/stores/roadmap.ts
import { atom } from 'nanostores';
export type RoadmapProgress = {
done: string[];
learning: string[];
skipped: string[];
personalized: { topicIds: string[]; information: string };
};
export const roadmapProgress = atom<RoadmapProgress>({
done: [],
learning: [],
skipped: [],
personalized: { topicIds: [], information: '' }
});
export const totalRoadmapNodes = atom<number>(0);
Consuming State in React Components
Components access these stores using the useStore hook. For example, PageProgress.tsx displays loading states using a separate UI-focused store from src/stores/page.ts.
// src/components/PageProgress.tsx
import { useStore } from '@nanostores/react';
import { pageProgressMessage } from '../stores/page';
export function PageProgress() {
const $msg = useStore(pageProgressMessage);
if ($msg === undefined) return null;
return <div className="fixed top-0 left-0 w-full bg-blue-100 p-2">{ $msg }</div>;
}
Visual Progress Rendering and DOM Manipulation
Beyond state management, the system applies visual cues by manipulating DOM elements directly. The renderTopicProgress function toggles CSS classes (done, learning, skipped, removed) on topic elements based on their current state. After updates, refreshProgressCounters walks the DOM, counts elements by class, and updates progress percentages in [data-progress-*] attributes.
This hybrid approach—Nanostores for reactive state and direct DOM manipulation for styling—ensures high-performance updates without triggering full component re-renders for every topic node.
Handling Large Roadmap Migrations
For very large migrated roadmaps, the client implements a cleanup safeguard in clearMigratedRoadmapProgress. If a user clears their progress 10 times, the client stores this count in localStorage and subsequently drops remote progress tracking for that specific roadmap. This prevents endless refetches for stale or deprecated roadmap content.
Summary
- No Zustand: The application uses Nanostores for reactive state and React Query for server-state management.
- Dual Persistence: Progress is stored in a remote database via JWT-protected endpoints and cached locally in Nanostores.
- Instant UI Updates: The
roadmapProgressandtotalRoadmapNodesstores trigger immediate re-renders in components usinguseStore. - Cache Synchronization: After every update,
queryClient.setQueryDataensures React Query hooks receive fresh data without network refetching. - Key Files: Core logic resides in
src/lib/resource-progress.ts, store definitions insrc/stores/roadmap.ts, and API integration insrc/queries/resource-progress.ts.
Frequently Asked Questions
Does developer-roadmap use Zustand for progress tracking?
No. Despite the popularity of Zustand in React applications, this codebase explicitly uses Nanostores for client-side reactive state management. The architecture favors Nanostores' atomic, framework-agnostic approach combined with React Query for server-state synchronization.
How does the application handle authentication for progress persistence?
The system checks for a JWT cookie (TOKEN_COOKIE_NAME) using the js-cookie library. If present, API requests include the user's identity, allowing the backend to return and update user-specific progress arrays. Unauthenticated users receive empty defaults and cannot persist progress to the database.
What happens when a user toggles a topic's completion status?
The UI invokes updateResourceProgress with the topic metadata and new state. This function POSTs to /v1-update-resource-progress, receives the updated arrays from the server, writes them to the roadmapProgress Nanostore for immediate UI feedback, and updates the React Query cache via queryClient.setQueryData to maintain consistency across all data-fetching components.
Where are the progress stores defined and how are they accessed?
The primary stores are defined in src/stores/roadmap.ts:
roadmapProgress– Tracksdone,learning,skipped, andpersonalizedtopic arraystotalRoadmapNodes– Stores the total count for percentage calculationspageProgressMessage– UI-specific loading messages defined insrc/stores/page.ts
Components access these using useStore from @nanostores/react, as demonstrated in src/components/PageProgress.tsx and src/components/TopicDetail/TopicDetail.tsx.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →