@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;
};