@formations-embedded/react
v0.1.1
Published
React component library for embedding Formations S-Corp workflows into partner apps.
Readme
@formations-embedded/react
React component library partners install to embed Formations S-Corp workflows directly inside their own product.
This package is currently
0.1.0— pre-publish. Public API is liable to change until we tag1.0.0.
Installation
npm install @formations-embedded/react react react-domMaterial UI, Emotion, and React Query are bundled inside the
package. The library only declares react and react-dom (>=17) as peer
dependencies.
Usage
import { Dashboard, Formations } from "@formations-embedded/react";
import "@formations-embedded/react/index.css";
function HomeDashboard({ accessToken }: { accessToken: string }) {
return (
<Formations environment="SANDBOX" accessToken={accessToken}>
<Dashboard />
</Formations>
);
}That's the minimum needed to render an embedded dashboard. <Formations>
wraps children in the shared Formations MUI theme automatically — you do
not need a separate ThemeProvider unless you want to customize it (see
Material UI theme).
see Token lifecycle below.
Material UI theme
Embedded components use Material UI with the same Formations design
tokens as the main app. <Formations> applies FormationsThemeProvider
(ThemeProvider + StyledEngineProvider injectFirst) automatically.
To reuse the theme in tests or Storybook:
import {
FormationsThemeProvider,
theme,
FormationsPrimaryButton,
} from "@formations-embedded/react";
import { MockTheme } from "@formations-embedded/react/testing";
render(
<MockTheme>
<Dashboard />
</MockTheme>,
);Export theme, palette, typography, and button helpers when building
custom embed UI that should match Formations styling.
Icons (FormationsIcon) live in a separate entry so the default embed
bundle stays small — import only when needed:
import { FormationsIcon } from "@formations-embedded/react/icon";<Formations> props
| Prop | Type | Required | Description |
| --------------------- | ----------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| environment | "SANDBOX" \| "PRODUCTION" | yes | Which Formations backend the API client targets. |
| accessToken | string | yes | A customer-scoped JWT minted by your backend. |
| refreshAccessToken | (ctx) => Promise<string> | no | Called when the token is about to expire or after a 401. Resolve with a fresh token to keep the session alive. |
| refreshLeadTimeMs | number | no | How early (ms) before exp to refresh proactively. Defaults to 60_000. |
| apiBaseUrlOverride | string | no | Point the client at a non-default base URL (useful when developing against a local Formations backend). |
| onError | (error: FormationsError) => void | no | Notified for unrecoverable transport / auth errors. |
| queryClient | QueryClient | no | React Query client instance. Defaults to the package singleton. Import QueryClient from this package if you need a shared client. |
React Query
React Query v3 is bundled — you do not install it separately. Import
hooks and types from @formations-embedded/react:
import { useQuery, useQueryClient, queryClient, type UseQueryOptions } from "@formations-embedded/react";<Formations> wraps children in a QueryClientProvider using the shared
queryClient singleton (same defaults as the main Formations app: 5-minute
staleTime / cacheTime, no retries, no refetch on window focus).
Data fetching follows the same three-layer pattern as the main codebase:
Component → hooks/api/* → services/*Query hooks
import { useMe, useDashboard } from "@formations-embedded/react";
function Profile() {
const { me, isLoading, error } = useMe();
const { dashboard, refetch } = useDashboard();
// ...
}Adding new hooks
- Add a service function in
services/that acceptsApiClient. - Create
hooks/api/useXxx.tswrappinguseQuery/useMutation. - Export from the package
index.tswhen it's part of the public API.
// hooks/api/useWidgets.ts (template)
import {
useQuery,
useQueryClient,
useMutation,
type UseQueryOptions,
useFormations,
} from "@formations-embedded/react";
import { getWidgets, createWidget } from "../../services/widgets";
export const useWidgets = (options?: UseQueryOptions<Widget[]>) => {
const { api, accessToken } = useFormations();
const { data, ...rest } = useQuery<Widget[]>(
["widgets", accessToken],
() => getWidgets(api),
{ enabled: !!accessToken, ...options },
);
return { widgets: data ?? [], ...rest };
};Cache management
Import the singleton when you need cache operations outside React:
import { queryClient } from "@formations-embedded/react";
queryClient.invalidateQueries(["dashboard"]);Inside hooks, prefer useQueryClient() from @formations-embedded/react.
Sharing a query client
<Formations> wraps children in its own QueryClientProvider by default.
Pass queryClient to share a single client across multiple embeds, or
create one with the re-exported QueryClient class:
import { Formations, QueryClient } from "@formations-embedded/react";
const sharedClient = new QueryClient();
<Formations queryClient={sharedClient} ...>Testing
import {
createQueryWrapper,
mockedQueryClient,
} from "@formations-embedded/react/testing";
beforeEach(async () => {
await mockedQueryClient.resetQueries();
});
const { result } = renderHook(() => useMe(), {
wrapper: createQueryWrapper(),
});Token lifecycle
- Your backend mints a short-lived (≤ 60 min) customer-scoped JWT by
calling Formations' partner endpoints — see
BACKEND_API.mdsection 1. - You pass that JWT in as
accessToken. The library decodes theexpclaim locally to schedule proactive refresh. refreshLeadTimeMsbeforeexp, the library invokes yourrefreshAccessTokencallback. Your callback typically fetches a new token from your own backend (which performs the S2S exchange).- If a request happens to race the refresh and gets a
401, the library callsrefreshAccessToken(sharing the in-flight promise if one is already running) and retries the request exactly once. - If
refreshAccessTokenrejects or isn't provided, the library firesonErrorwithcode: "AUTH_FAILED". The host app is responsible for re-authenticating the user.
<Formations
environment="SANDBOX"
accessToken={accessToken}
refreshAccessToken={async ({ reason }) => {
// Call YOUR backend, not Formations directly. Your backend then
// does the S2S exchange with Formations.
const res = await fetch("/api/formations-token", {
headers: { "x-refresh-reason": reason },
});
if (!res.ok) throw new Error(`refresh failed: ${res.status}`);
const body = await res.json();
return body.accessToken;
}}
onError={(err) => console.error("[Formations]", err)}
>
<Dashboard />
</Formations>Why not have the library refresh the token itself?
Refreshing requires partner credentials, and partner credentials must never live in the browser. The library deliberately delegates refresh to your backend so secrets stay on the server.
Components
<Dashboard />
Renders the S-Corp summary view. Loads
GET /api/embedded/v1/dashboard on mount and re-renders when the
token rotates. See DashboardData for the response shape.
Hooks (advanced)
useMe() / useDashboard()
Typed React Query hooks for the built-in endpoints. Return domain-named
data plus standard query fields (isLoading, error, refetch, etc.).
useFormations()
Returns { environment, accessToken, api, tokenManager }. Use
api.request<T>(path, opts) to call any Formations endpoint with the
current access token (it handles 401 → refresh → retry for you).
import { useFormations, useApiResource } from "@formations-embedded/react";
function MyEmbed() {
const { api } = useFormations();
const { data, loading, refetch } = useApiResource<MyShape>("/api/embedded/v1/something");
// ...
}useApiResource<T>(path)
React Query-backed generic fetcher for arbitrary paths. Automatically
re-runs when the token rotates. Returns { data, error, loading, refetch }.
Prefer typed hooks in hooks/api for known endpoints.
Errors
All recoverable failures resolve to a FormationsError:
interface FormationsError {
code: "AUTH_FAILED" | "TOKEN_EXPIRED" | "NETWORK_ERROR" | "API_ERROR" | "CONFIG_ERROR";
message: string;
status?: number;
cause?: unknown;
}The onError callback on <Formations> receives these as they happen.
Hooks like useApiResource also expose the error via their error
field.
Backend dependency
The library expects specific endpoints on the Formations backend. See
BACKEND_API.md for the full contract — that file
is the source of truth for backend implementation work.
Development
The fastest inner loop is to run the sample app and edit library
source directly — Vite aliases @formations-embedded/react to this
package's src/ so HMR fires on every save:
# from repo root
npm install
npm run dev # opens the DevUI playground at http://localhost:5173For raw build / typecheck:
npm run build --workspace @formations-embedded/react
npm run typecheck --workspace @formations-embedded/reactBundle size
The production build minifies and tree-shakes dependencies. Approximate
sizes after npm run build --workspace @formations-embedded/react:
| Artifact | Minified | Gzipped |
| -------- | -------- | ------- |
| Main (index.js) | ~230 KB | ~73 KB |
| Icons (icon.js + font) | ~4.2 MB | ~1.5 MB |
| Testing (testing.js) | ~116 KB | — |
The main bundle includes MUI, Emotion, and React Query. Material Symbols
fonts are not loaded unless you import @formations-embedded/react/icon.
Test helpers import from @formations-embedded/react/testing.
