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-registry

react, 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#

app.css
@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:

setup.ts
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(),
})
NameTypeDefaultDescription
baseUrlstringthe current originRoot URL of the API.
getAuthToken() => string | undefinedBearer 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.
dialectApiDialectInterfacejsonLdDialect()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.

setup.ts
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(),
})
NameTypeDefaultDescription
baseUrlstringthe current originRoot URL of the API.
getAuthToken() => string | undefinedBearer token attached to every request. Return undefined when nobody is signed in.
getScope() => string | undefinedValue of the X-Scope header, for an API that segments its responses.
mercurePathstringPath 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#

ArticlesPage.tsx
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 resourceId that 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 member and totalItems, Strapi { data, meta }, Supabase a bare array: check the one you configured matches the one that answered.
  • Unstyled markup — a missing @source line.

Order of setup

  1. configureApi — where the API is, and which dialect it speaks.
  2. configurePorts — router, metadata, locale.
  3. createViewResource — your resources, at module scope.
  4. 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.