# User Progress Tracking and Persistence in developer-roadmap

> Discover how the developer-roadmap app tracks user progress using Nanostores and React Query. Learn about its state management and API persistence beyond common assumptions.

- Repository: [Kamran Ahmed/developer-roadmap](https://github.com/kamranahmedse/developer-roadmap)
- Tags: how-to-guide
- Published: 2026-02-24

---

**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](https://github.com/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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/queries/resource-progress.ts)) manages server-state synchronization, background caching, and query invalidation.
- **REST API** endpoints (`/v1-get-user-resource-progress` and `/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:

1. **Resource Identification**: The UI identifies the context using `resourceType` (`roadmap` or `best-practice`), `resourceId`, and `topicId` via the `TopicMeta` type in [`src/lib/resource-progress.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/lib/resource-progress.ts).

2. **Authentication Check**: The system verifies the presence of a JWT authentication cookie (`TOKEN_COOKIE_NAME`) using `Cookies.get`. If absent, progress operations return empty defaults and disable persistence.

3. **Initial Data Fetch**: For authenticated users, `getResourceProgress` executes a GET request to `/v1-get-user-resource-progress`, returning arrays of topic IDs categorized as `done`, `learning`, `skipped`, and a `personalized` object.

4. **Store Hydration**: The response populates two Nanostores defined in [`src/stores/roadmap.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/stores/roadmap.ts):
   - `roadmapProgress` – Contains the four state arrays
   - `totalRoadmapNodes` – Tracks the total topic count for percentage calculations

5. **UI Rendering**: Components consuming these stores via `useStore` from `@nanostores/react` automatically re-render. Visual states are applied through CSS classes (`done`, `learning`, `skipped`, `removed`) manipulated by `renderTopicProgress`.

6. **Progress Updates**: When users toggle topics, `updateResourceProgress` sends a POST request to `/v1-update-resource-progress` with the new state.

7. **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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/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.

```typescript
// 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.

```typescript
// 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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/stores/roadmap.ts). These exports provide the reactive primitives that components subscribe to using the `@nanostores/react` hook.

```typescript
// 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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/PageProgress.tsx) displays loading states using a separate UI-focused store from [`src/stores/page.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/stores/page.ts).

```tsx
// 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 `roadmapProgress` and `totalRoadmapNodes` stores trigger immediate re-renders in components using `useStore`.
- **Cache Synchronization**: After every update, `queryClient.setQueryData` ensures React Query hooks receive fresh data without network refetching.
- **Key Files**: Core logic resides in [`src/lib/resource-progress.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/lib/resource-progress.ts), store definitions in [`src/stores/roadmap.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/stores/roadmap.ts), and API integration in [`src/queries/resource-progress.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/stores/roadmap.ts):
- `roadmapProgress` – Tracks `done`, `learning`, `skipped`, and `personalized` topic arrays
- `totalRoadmapNodes` – Stores the total count for percentage calculations
- `pageProgressMessage` – UI-specific loading messages defined in [`src/stores/page.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/stores/page.ts)

Components access these using `useStore` from `@nanostores/react`, as demonstrated in [`src/components/PageProgress.tsx`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/components/PageProgress.tsx) and [`src/components/TopicDetail/TopicDetail.tsx`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/components/TopicDetail/TopicDetail.tsx).