Skip to content

@msflib/react-curriculum

Standalone curriculum module built on TanStack Query.

Install

pnpm add @msflib/react-curriculum

Peer deps

  • react >= 18
  • @tanstack/react-query >= 5

Usage

Wrap your app with the module Provider inside your QueryClientProvider.

In Next.js App Router, Provider must be in a client component ("use client").

Topics are always listed scoped to one level + track combination — the backend requires level_id and track_id as query params on the list endpoint, there is no "list all topics" route. Pass them via options.query; the initial fetch stays disabled until both are supplied.

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { CurriculumProvider, useCurriculum } from '@msflib/react-curriculum';

const queryClient = new QueryClient();

<QueryClientProvider client={queryClient}>
  <CurriculumProvider
    options={{
      requireAuth: true,
      isAuthenticated: true,
      query: { level_id: 1, track_id: 2 },
    }}
  >
    <TopicList />
  </CurriculumProvider>
</QueryClientProvider>;

function TopicList() {
  const { topics, loading, createTopic, updateTopic, deleteTopic } =
    useCurriculum();

  if (loading.topics) return <p>Loading…</p>;

  return (
    <ul>
      {topics.map((topic) => (
        <li key={topic.id}>
          {topic.label} — {topic.title}
          <button disabled={loading.delete} onClick={() => deleteTopic(topic.id)}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

Switching level/track on demand

Use listTopics to fetch a different level/track combination without remounting the provider (e.g. a level/track picker):

const { listTopics } = useCurriculum();

await listTopics({ level_id: 3, track_id: 4, offset: 0, limit: 50 });

This is a plain on-demand fetch (its result is not written into topics — swap options.query on the Provider if you want the on-mount list itself to reflect the new level/track).

Fetching a single topic

Use getTopic from context for an on-demand fetch (e.g. triggered by a user action), or the standalone useTopic hook for a declarative, cache-backed read:

import { useTopic } from '@msflib/react-curriculum';

function TopicDetail({ topicId }: { topicId: number }) {
  const { data: topic, isPending } = useTopic(topicId);

  if (isPending) return <p>Loading…</p>;
  return <h2>{topic?.title}</h2>;
}

useTopic is disabled (no request fired) while topicId is undefined.

Options

Key Type Default Description
isWorkspaceScoped boolean true If true, topics are isolated per active workspace.
requireAuth boolean false If true, topics are only fetched once isAuthenticated is true.
isAuthenticated boolean false The authentication status of the user (e.g. from useAuth().status).
query object — { level_id, track_id, offset?, limit? } for the initial topics fetch. level_id/track_id are required — the initial fetch is disabled until both are set.

Endpoints

Configure paths through configureApplication({ endpoints: { curriculum } }).

Key Default path Methods / routes
topics /curriculum/topics list (GET, requires level_id/track_id, supports offset/limit) / create (POST) / detail, update, delete (GET | PUT | DELETE on /{id})

deleteTopic resolves with the deleted Topic, matching the backend response (not void).

TypeScript types

export type Subtopic = {
  id: number;
  created_at: string;
  updated_at: string;
  title: string;
  topic_id: number;
  order: number;
  [k: string]: unknown;
};

export type SubtopicCreatePayload = {
  title: string;
  order: number;
};

export type SubtopicUpdatePayload = {
  id?: number;
  title?: string;
  order?: number;
};

export type Topic = {
  id: number;
  created_at: string;
  updated_at: string;
  title: string;
  level_id: number;
  track_id: number;
  label: string;
  order: number;
  subtopics: Subtopic[];
  [k: string]: unknown;
};

export type TopicCreatePayload = {
  title: string;
  level_id: number;
  track_id: number;
  label: string;
  order: number;
  subtopics?: SubtopicCreatePayload[];
};

export type TopicUpdatePayload = Partial<{
  title: string;
  level_id: number;
  track_id: number;
  label: string;
  order: number;
  subtopics: SubtopicUpdatePayload[];
}>;

export type TopicListParams = {
  level_id: number;
  track_id: number;
  offset?: number;
  limit?: number;
};