@peerfold/react
v0.2.0
Published
React provider + hooks for the Peerfold API, built on @peerfold/api-client. Plain React, no data-fetching runtime dependency; SSR-safe.
Readme
@peerfold/react
React provider + hooks over @peerfold/api-client. Built with
plain React — no data-fetching runtime dependency — and SSR-safe (no
window at module scope; fetching runs in effects, so a server render just
returns the initial loading state).
Deviation from the PRD (§5.3): the PRD sketched TanStack Query "under the hood". We keep the core zero-dependency instead — the hooks are a thin, auditable
useEffect+AbortControllerlayer. Drop TanStack Query in at the app level if you want its cache; these hooks don't require it.
Install
npm install @peerfold/react @peerfold/api-clientreact (18 or 19) is a peer dependency; @peerfold/api-client is what you build
the client with.
A note on naming (0.2.0). The component and class names are
PeerfoldProvider/PeerfoldClient/PeerfoldError, the hook isusePeerfoldClient, the wire headers arePeerfold-Version/X-Peerfold-*and the browser storage keys arepeerfold.*. 0.1.x shipped the oldHubLMS-prefixed names — this is a breaking rename, so pin^0.2.0.
Setup
Build a client (with a learner token from your auth flow) and pass it to the provider. The provider constructs nothing itself, so it is safe to render on the server.
import { PeerfoldClient } from "@peerfold/api-client";
import { PeerfoldProvider } from "@peerfold/react";
const client = new PeerfoldClient({
baseUrl: "https://acme.site.hublms.com",
learnerToken: () => tokenStore.get(), // provider fn → always reads the current token
});
export function App() {
return (
<PeerfoldProvider client={client}>
<Catalog />
</PeerfoldProvider>
);
}Read hooks
Each returns { data, error, loading, reload }.
import { useMe, useCatalog, useCourse, useEnrollments, useCertificates } from "@peerfold/react";
function Catalog() {
const { data: courses, loading, error, reload } = useCatalog();
if (loading) return <Spinner />;
if (error) return <Error msg={error.message} onRetry={reload} />;
return <ul>{courses!.map((c) => <li key={c.id}>{c.title}</li>)}</ul>;
}
function CoursePage({ slug }: { slug: string }) {
const { data: course } = useCourse(slug); // fetch is skipped while slug is falsy
const { data: me } = useMe();
// …
}useCatalog, useEnrollments, useCertificates eagerly collect all pages via
the client's iterate(); use client.*.iterate() directly for manual paging.
Optimistic progress
useProgressMutation is an optimistic, coalescing, retrying queue for progress
events — call record freely from the player:
import { useProgressMutation } from "@peerfold/react";
function Lesson({ enrollmentId, lessonId }: { enrollmentId: string; lessonId: string }) {
const { record, pending, error } = useProgressMutation({
onSuccess: (result) => setProgress(result.percent_complete ?? 0),
});
return (
<button
onClick={() =>
record(enrollmentId, { event_id: crypto.randomUUID(), type: "lesson_completed", lesson_id: lessonId })
}
>
Mark complete {pending > 0 ? `(syncing ${pending}…)` : ""}
</button>
);
}What it does:
- Coalesces redundant events — repeated records for the same (enrollment, lesson, type) collapse to the latest one still queued.
- Stable idempotency key per item — retries replay the server's stored response instead of double-appending.
- Retries 429/5xx and transient network errors with exponential backoff,
then surfaces the failure via
onError. - Optimistic —
recordreturns immediately; your UI advances without awaiting the network.
