f-ui

Integrating Into Your App

End-to-end host wiring for Next.js or TanStack apps—new or existing—with f-ui list pages, i18n, LinkProvider, and stock shadcn primitives.

Use this guide when you are adding f-ui to a host app (greenfield or an existing shadcn + React project) and want list pages, forms, and detail views to look and behave consistently.

If you only need a single open-registry item (Date Picker, Multi-Select, etc.), start with Installation instead.

When To Use This Guide

  • You already use shadcn/ui (or just ran shadcn init) and want a QueryList browse page (filter bar + table + pagination).
  • You use Next.js App Router or TanStack Start / TanStack Router with React 19.
  • You need Plus registry items (Table, Query List, Form Page, Descriptions, …).
  • You need Formily validators to track locale switches, or clipped focus rings in the filter bar — this guide covers those host wiring fixes.

Quick Checklist

  1. Confirm Tailwind + React + shadcn (components.json, @/components/ui) — run shadcn init only if the host is not set up yet.
  2. Add @f-ui and @f-ui-plus to components.json (Plus needs FUI_PLUS_REGISTRY_TOKEN).
  3. Install @f-ui-plus/query-list (pulls Formily, Query Filter, Table, Data List Chrome, Page).
  4. Mount <I18nProvider locale={…}> at the app root.
  5. SPA hosts: mount <LinkProvider linkComponent={YourRouterLink}> next to I18nProvider (same shell). Without it, backHref / table href / Copyable are native <a> and reload the document.
  6. Add host src/lib/app-locale.ts so Formily validators track locale switches.
  7. Leave components/ui/* as stock shadcn — do not hand-edit primitives.
  8. Add --fui-control-h* CSS tokens to your global stylesheet (f-ui-owned sizing).
  9. Wire your first list route — see CRUD Page Patterns.

1. Prepare the Host App

f-ui assumes a standard shadcn layout: Tailwind v4 (or v3 with CSS variables), @/components/ui, @/lib/utils.

Existing app: skip scaffolding if those pieces are already in place; jump to registries (§2).

New app: scaffold, then initialize shadcn:

Create an App Router project, then initialize shadcn:

npx create-next-app@latest my-app
cd my-app
npx shadcn@latest init

Follow the TanStack Start scaffold, then:

pnpm dlx shadcn@latest init

Set "rsc": false in components.json if you are not using React Server Components for UI files.

Any Vite + React app works as long as path aliases match components.json (@/components, @/lib, …).

2. Configure Registries

Copy this components.json registries block (adjust tailwind.css path to your project):

{
  "registries": {
    "@f-ui": "https://ui.isaacfei.com/r/{name}.json",
    "@f-ui-plus": {
      "url": "https://ui.isaacfei.com/api/plus/r/{name}.json",
      "headers": {
        "Authorization": "Bearer ${FUI_PLUS_REGISTRY_TOKEN}"
      }
    }
  }
}

Plus token

  1. Create a token on the tokens page.
  2. Export FUI_PLUS_REGISTRY_TOKEN in .env.local or your shell.
  3. Verify:
curl -H "Authorization: Bearer $FUI_PLUS_REGISTRY_TOKEN" \
  https://ui.isaacfei.com/api/plus/r/query-list.json

See Installation — Plus Registry for CI/CD notes.

3. Install the List-Page Stack

Install query-list once; the CLI resolves Plus dependencies (Formily, Query Filter, Table, Data List Chrome, Page, and open-registry peers such as Select and Date Range Picker):

FUI_PLUS_REGISTRY_TOKEN=xxx pnpm dlx shadcn@latest add @f-ui-plus/query-list
FUI_PLUS_REGISTRY_TOKEN=xxx npx shadcn@latest add @f-ui-plus/query-list

Optional add-ons for a full CRUD loop:

GoalRegistry item
Read-only detail grid@f-ui-plus/descriptions
Create / edit full page@f-ui-plus/form-page
Card-style rows instead of tableQueryList view="list" (same @f-ui-plus/query-list install). Object Page regions use Related List + List — see List Surfaces

Component reference: Query List, Form Page.

4. Wire Internationalization

f-ui strings are separate from your app i18n. Mount the provider once around routes that render f-ui:

import { I18nProvider } from "@/components/f-ui/i18n/i18n-provider";

export function AppProviders({
  locale,
  children,
}: {
  locale: string;
  children: React.ReactNode;
}) {
  return <I18nProvider locale={locale}>{children}</I18nProvider>;
}
  • locale must update on language switch (re-render the provider), not a one-off getLocale() read. See Internationalization.
  • Optional t prop bridges your message catalogs; built-in English and Chinese bundles cover defaults.

Host app-locale adapter (required for Formily)

Vendored Formily imports @/lib/app-locale. Your app must provide this module so validator messages follow the active locale.

Contract: useAppLocale() returns the current locale string and subscribes so Form trees re-render when language changes without a full reload. A one-off getLocale() in a parent that never re-renders is not enough.

Paraglide (Next.js or TanStack)

Paraglide is not TanStack-only. On Next.js App Router (or TanStack Start / Router), use the same cookie + client notify pattern this docs site uses:

// src/lib/app-locale.ts
"use client";

import { useSyncExternalStore } from "react";
import { getLocale } from "@/paraglide/runtime";

const listeners = new Set<() => void>();

export function notifyAppLocaleChanged() {
  listeners.forEach((listener) => listener());
}

function subscribe(onStoreChange: () => void) {
  listeners.add(onStoreChange);
  return () => {
    listeners.delete(onStoreChange);
  };
}

export function useAppLocale(): string {
  return useSyncExternalStore(subscribe, getLocale, getLocale);
}

After setLocale(next, { reload: false }), call notifyAppLocaleChanged() so useAppLocale() / I18nProvider see the new value. Wire that next to your language switcher (Language Selector, navbar, etc.).

TanStack hosts that already re-render on navigation can additionally subscribe with useRouterState() (or equivalent) if locale is also encoded in the URL — the Paraglide notify path above is still required for reload: false cookie switches.

Next.js without Paraglide (locale segment)

If you drive locale only from the App Router segment (no Paraglide):

// src/lib/app-locale.ts
"use client";

import { useParams } from "next/navigation";

export function useAppLocale(): string {
  const params = useParams();
  return typeof params.locale === "string" ? params.locale : "en";
}

If this file is missing, shadcn add succeeds but Formily validation stays in the wrong language after switching locales.

Install the open-registry link item (@f-ui/link) so Page back links, link cells, Copyable, and row href actions use your client-side router.

Resolution order: prop linkComponentLinkProvider → native "a".

SPA hosts must mount LinkProvider

A missing provider is a native document link, not a no-op. Clicking Page Back, a table href, or Copyable then reloads the whole app (sidebar, session /me, list skeletons). That is the most common “list → detail → Back flashes the shell” bug.

MPA / static sites may omit the provider on purpose. SPA hosts (Next App Router, TanStack Router / Start) must mount it next to I18nProvider. In development, f-ui logs a one-time console warning when it falls back to "a" without a provider or a local linkComponent="a".

What Goes Wrong

Host setupWhat the operator sees
LinkProvider + router LinkClient navigation. Sidebar stays. List cache / keep-mounted layouts survive.
No provider (SPA)Browser follows <a href>. Full document load. Session, KB switcher, and lists remount.
Provider mounted but adapter ignores hrefSame as native <a> if the adapter does not intercept the click.

Pass linkComponent="a" (or a local native adapter) on a single surface when a real document link is intentional — that silences the DEV warning for that tree.

Next.js App Router

The adapter and provider must live in a Client Component boundary (file starts with "use client"):

"use client";

import { forwardRef } from "react";
import Link from "next/link";
import { LinkProvider } from "@/components/f-ui/link/link-provider";
import type { LinkComponentProps } from "@/components/f-ui/link/link-types";

const RouterLink = forwardRef<HTMLAnchorElement, LinkComponentProps>(
  function RouterLink(props, ref) {
    return <Link {...props} ref={ref} />;
  },
);

export function AppProviders({ children }: { children: React.ReactNode }) {
  return <LinkProvider linkComponent={RouterLink}>{children}</LinkProvider>;
}

The host wrapper owns Next prefetch defaults. Dense tables may disable viewport prefetch to avoid flooding the network with row/link prefetch work.

TanStack Router

f-ui only emits absolute or app-relative href strings. TanStack’s typed in-app destination is to. The host adapter must put that string on the channel your router version actually intercepts.

If Back still reloads the document, the adapter is painting href on a native attribute only. Map hrefto (typical), or spread the untyped href channel if that version treats it as a destination:

import { forwardRef } from "react";
import { Link } from "@tanstack/react-router";
import type { LinkComponentProps } from "@/components/f-ui/link/link-types";

const RouterLink = forwardRef<HTMLAnchorElement, LinkComponentProps>(
  function RouterLink({ href, children, ...props }, ref) {
    return (
      <Link ref={ref} to={href} {...props}>
        {children}
      </Link>
    );
  },
);

Verify in DevTools: the Back control is a TanStack Link (data-status) and a left-click does not trigger a document navigation. Middle-click / open in new tab must still work (one <a>, forwarded ref).

FormPage guard composition

When unsaved-change interception must cover generated back / row / cell links, mount the router-specific guard Link inside the guard provider:

import {
  FormNavigationGuardProvider,
  GuardedLink,
} from "@/components/f-ui/form-page/navigation-guard/next-app-router";
import { LinkProvider } from "@/components/f-ui/link/link-provider";

<FormNavigationGuardProvider>
  <LinkProvider linkComponent={GuardedLink}>{children}</LinkProvider>
</FormNavigationGuardProvider>;

GuardedLink (Next App Router adapter) forwards the anchor ref and props while intercepting onNavigate when the form is dirty.

5. Respect Host Shadcn Primitives

Do not hand-edit `components/ui/*`

Treat everything under src/components/ui/ as vendored shadcn. Keep the files upstream-verbatim from shadcn init / shadcn add. Do not patch class strings, props, or behavior by hand to match an older f-ui dense contract.

f-ui recipes compose on top of your host primitives (Input, Button, Dialog, …). Visual or behavioral changes belong in:

  • f-ui / app composition — wrappers, className at the call site, CSS that targets data-slot where the primitive exposes it
  • CSS tokens (§6) — f-ui-owned control heights and gaps
  • Fresh registry pullspnpm exec shadcn add <component> --overwrite when you intentionally refresh a primitive

Do not copy src/components/ui/ from this docs repo into your app, and do not “restore” local forks after shadcn add. If something looks off after a pull, reinstall the current @f-ui / @f-ui-plus item instead of forking the primitive.

6. CSS Design Tokens

Add f-ui control height tokens to your global CSS (e.g. src/app/globals.css or src/styles.css):

:root {
  --fui-control-h: 2rem;
  --fui-control-h-sm: 1.75rem;
  --fui-control-h-lg: 2.25rem;
  --fui-form-action-gap: 0.5rem;
}

Date pickers, multi-select anchors, and data-list toolbar buttons read these variables for f-ui-owned shells. They do not require editing host components/ui/*. Without the tokens, those f-ui surfaces fall back to hard-coded Tailwind classes.

7. First List Page (Minimal Wiring)

  1. Define a data-list schema (defineDataListSchema) — columns and filter fields share one schema. See Table and /showcases/orders.
  2. Hold list params in React stateuseListState() (from data-list internals) gives { params, onParamsChange }. URL sync is opt-in; default keeps list URLs clean.
  3. Implement a DataListAdapter — connects TanStack Query (or your fetch layer) to pagination, sort, and filter params emitted by Query List.
  4. Render QueryList inside PageContainer:
import { PageContainer } from "@/components/f-ui/page/page-container";
import { QueryList } from "@/components/f-ui/query-list/query-list";
import { Statistic } from "@/components/f-ui/statistic/statistic";
import { StatisticGroup } from "@/components/f-ui/statistic/statistic-group";
import { useListState } from "@/components/f-ui/data-list-internals/hooks/use-list-state";
import { defineDataListSchema } from "@/components/f-ui/data-list-internals/schema/define-data-list-schema";

const ordersSchema = defineDataListSchema({
  /* columns + filters */
});

export function OrdersPage() {
  const { params, onParamsChange } = useListState();
  const adapter = useOrdersAdapter({ params }); // your TanStack Query hook → DataListAdapter
  // summary = useQuery({ queryKey: ["orders-summary", params], … })

  return (
    <PageContainer title="Orders">
      <QueryList
        listCode="orders"
        schema={ordersSchema}
        adapter={adapter}
        params={params}
        onParamsChange={onParamsChange}
        getRowId={(row) => row.id}
        stats={
          <StatisticGroup>
            <Statistic variant="card" title="Orders" value={summary?.orderCount ?? null} />
          </StatisticGroup>
        }
      />
    </PageContainer>
  );
}
  1. Invalidate after mutations — use a stable listCode and query-key prefix so create/update/delete refreshes the table.

Full recipe (toolbar, batch actions, footer inset): Query List. Architecture: CRUD Page Patterns.

When you need a newer shadcn primitive, refresh it with the CLI — never by hand-editing the generated file:

pnpm exec shadcn add button --overwrite

Host apps do not need to mirror this docs repo’s internal verify:primitives checks on input / button / textarea. Those guards are for this repository’s own CI. In your app:

  • Keep components/ui/* as shadcn delivers them
  • Reinstall current @f-ui / @f-ui-plus items if a recipe looks wrong after a primitive refresh
  • Prefer fixing clipped rings / layout bugs by updating Query Filter (or the owning f-ui item), not by forking ui/*

Troubleshooting

SymptomLikely causeFix
Focus ring clipped in filter barStale Query Filter wrapperReinstall current @f-ui-plus/query-filter
Plus add returns 401Missing / expired tokenSet FUI_PLUS_REGISTRY_TOKEN; curl-verify (§2)
f-ui labels English after switching to 中文I18nProvider locale prop staleRe-render provider on locale change (i18n)
Formily validation language wrongMissing @/lib/app-localeAdd host adapter (§4)
Tempted to patch ui/button after addLocal fork habitLeave stock; compose outside components/ui/ (§5)

Next Steps

On this page