react-resource-view
Installation
Install, style, and connect the package to your API.
Install#
pnpm add react-resource-view react-data-form react-mini-i18n resource-registryreact, react-data-form, react-mini-i18n and resource-registry are peer dependencies. The last two own module-level singletons — a dictionary and a registry — so they have to resolve to a single copy. See the same note on the forms side.
TanStack Router is optional
The core never references it. Installing it is only needed if you import react-resource-view/tanstack, and an application on another router installs nothing extra.
Styles#
@import "tailwindcss";
@source "../node_modules/react-resource-view/dist";
@source "../node_modules/react-data-form/dist";
/* Only if your application has no shadcn theme of its own. */
@import "react-resource-view/styles.css";Both packages need the @source line — the views render form fields, so both sets of classes have to be generated. If your application already has a shadcn theme, leave the @import out and the components take your palette.
Connect a router#
The views navigate and build links, but the package knows no router. It asks for four primitives, and ships an adapter for TanStack Router:
import { configurePorts } from "react-resource-view"
import { tanstackAdapter } from "react-resource-view/tanstack"
configurePorts({ navigation: tanstackAdapter })With any other router, supply the four yourself — it is about fifteen lines, and the routing page walks through them.
Unconfigured, navigation is full page loads
Left without a navigation port, the views fall back to the History API and full reloads. Enough for a test or a story; not for production.
Point it at your API#
The API connection is configured separately — the views themselves never mention a URL. configureApi says where the API is and which dialect it speaks:
import { configureApi, strapiDialect } from "react-resource-view"
configureApi({
baseUrl: "https://api.example.com",
getAuthToken: () => (isLogged() ? getUserToken() : undefined),
// The default is jsonLdDialect(); strapiDialect() and supabaseDialect() ship too.
dialect: strapiDialect(),
})| Name | Type | Default | Description |
|---|---|---|---|
baseUrl | string | the current origin | Root URL of the API. |
getAuthToken | () => string | undefined | — | Bearer token attached to every request. Return undefined when nobody is signed in. |
getHeaders | () => Record<string, string> | {} | Extra headers on every request — a tenant header, a Supabase apikey supplied outside the dialect. |
dialect | ApiDialectInterface | jsonLdDialect() | How the API spells its URLs, its pages, its filters and its errors. See backends & dialects. |
On API Platform
The JSON-LD dialect is the default, and it goes through the client of jsonld-api-client — middleware, scope header and typed paths included. Configuring that client is enough; configureApi falls back to its settings when it is given none of its own.
import { configureClient } from "jsonld-api-client"
configureClient({
baseUrl: "https://api.example.com",
getAuthToken: () => (isLogged() ? getUserToken() : undefined),
// Sent as X-Scope, when your API segments its responses.
getScope: () => getCurrentScope(),
})| Name | Type | Default | Description |
|---|---|---|---|
baseUrl | string | the current origin | Root URL of the API. |
getAuthToken | () => string | undefined | — | Bearer token attached to every request. Return undefined when nobody is signed in. |
getScope | () => string | undefined | — | Value of the X-Scope header, for an API that segments its responses. |
mercurePath | string | — | Path of the Mercure hub. A view with behavior.eventSourced refetches when the hub says the collection changed. |
And the rest of the view configuration, which is about the application rather than the API:
configurePorts({
appName: "My application", // page title suffix
description: "…", // page metadata
appUrl: "https://app.example.com",// absolute links escaping an iframe
ownsDocumentHead: true, // false when your router owns <head>
isDev: import.meta.env.DEV, // development affordances
dateLocale: fr, // calendar and timeline month names
})ownsDocumentHead on a server-rendered app
Leave it true in a single-page application, where nothing else writes the head. Set it to false when your router declares metadata per route — otherwise both write it, the page ends up with two titles, and a crawler reads whichever came first.
Check it works#
import { ActionList } from "react-data-form"
import { createViewResource, ViewResourceContextProvider } from "react-resource-view"
const articles = createViewResource("articles", {
name: "Articles",
path: "/api/articles",
view: { form: { inputs: { title: { label: "Title" } } } },
})
export function ArticlesPage() {
return (
<ViewResourceContextProvider resource={articles} resourceAction={ActionList.list} />
)
}You should see a table with a create button and a filter bar. If not:
- “Form is needed for build a table” — the table's columns come from
view.form. Give the view a form. - “Resource not found” — the provider was given a
resourceIdthat is not in the registry. Pass the resource object, or register it through a scope. - An empty list against a working API — the dialect and the API disagree on the envelope. Hydra answers
memberandtotalItems, Strapi{ data, meta }, Supabase a bare array: check the one you configured matches the one that answered. - Unstyled markup — a missing
@sourceline.
Order of setup
configureApi— where the API is, and which dialect it speaks.configurePorts— router, metadata, locale.createViewResource— your resources, at module scope.- Render a view.
The first three are module-level singletons, so run them once, before the first render — in a file your entry point imports.