# Components

URL: https://developers.uidu.org/docs/tools/react/components

> Prebuilt React components for rendering uidu CMS content.

## `<PageBlocks pageBlocks components fallback? />`

Walks a page's `pageBlocks` array and renders each block using the component registered for its shortname. Returns null when `pageBlocks` is empty.

```tsx
import { getPage } from '@uidu/client';
import { PageBlocks } from '@uidu/react';
import { uidu, config } from '@/lib/uidu';
import { Hero, Feature, ContactForm } from '@/components/blocks';

export default async function HomePage() {
  const page = await getPage(uidu, {
    projectId: config.projectId,
    slug: 'home',
  });
  if (!page) return null;

  return (
    <PageBlocks
      pageBlocks={page.pageBlocks}
      components={{
        Header: Hero,
        feature1: Feature,
        ModuloContatto: ContactForm,
      }}
    />
  );
}
```

### How block components receive props

Each component receives the &#x2A;*raw `PageBlock`** as `props.block`:

```tsx
import type { Page } from '@uidu/client';
import { useFields, toText } from '@uidu/react';

type PageBlock = NonNullable<Page['pageBlocks']>[number];

export function Hero({ block }: { block: PageBlock }) {
  const fields = useFields(block.fieldValues);
  return <h1>{toText(fields.Titolo)}</h1>;
}
```

The block's shortname is derived from `block.templateBlock.block.shortname` first (the canonical block type), falling back to `block.shortname`.

### Handling unknown blocks

Pass a `fallback` to render a placeholder for blocks whose shortname isn't in your map:

```tsx
<PageBlocks
  pageBlocks={page.pageBlocks}
  components={blockComponents}
  fallback={({ shortname }) => (
    <div className="rounded border border-dashed p-3 text-xs">
      Unknown block: <code>{shortname}</code>
    </div>
  )}
/>
```

Without a fallback, unknown blocks are silently skipped.

## `<BlockRenderer pageBlocks shortname components />`

Render exactly one block by its shortname. Useful when blocks need to be placed in specific spots in your layout rather than rendered sequentially.

```tsx
<header>
  <BlockRenderer
    pageBlocks={page.pageBlocks}
    shortname="Header"
    components={{ Header: Hero }}
  />
</header>

<main>
  {/* other content */}
</main>

<aside>
  <BlockRenderer
    pageBlocks={page.pageBlocks}
    shortname="Sidebar"
    components={{ Sidebar: SidebarBlock }}
  />
</aside>
```

If no block matches the shortname, returns null.

## `<RichText doc />`

Render a Tiptap/ProseMirror JSON document as React elements. Used by uidu CMS rich-text fields (`Event.body`, `Call.body`, page block rich-text fields, etc.).

```tsx
import { getEvent } from '@uidu/client';
import { RichText } from '@uidu/react';
import { uidu } from '@/lib/uidu';

export default async function EventPage({ params }) {
  const event = await getEvent(uidu, { id: params.id });
  return (
    <article>
      <h1>{event?.name}</h1>
      <RichText doc={event?.body} />
    </article>
  );
}
```

### Supported nodes

| Tiptap type                  | Output                                     |
| ---------------------------- | ------------------------------------------ |
| `doc`                        | wrapping `<div>`                           |
| `paragraph`                  | `<p>`                                      |
| `heading` (level 1–6)        | `<h1>`–`<h6>`, scaled type                 |
| `bulletList` / `orderedList` | `<ul>` / `<ol>`                            |
| `listItem`                   | `<li>`                                     |
| `blockquote`                 | `<blockquote>` with left border            |
| `codeBlock`                  | `<pre><code>` with dark background         |
| `image`                      | `<img>` (`src`, `alt`, `title` from attrs) |
| `horizontalRule`             | `<hr>`                                     |
| `hardBreak`                  | `<br>`                                     |
| `text`                       | bare text node, marks wrap inside-out      |

### Supported text marks

| Tiptap mark                | Output                             |
| -------------------------- | ---------------------------------- |
| `bold` / `strong`          | `<strong>`                         |
| `italic` / `em`            | `<em>`                             |
| `underline`                | `<u>`                              |
| `strike` / `strikethrough` | `<s>`                              |
| `code`                     | inline `<code>` with pill styling  |
| `link`                     | `<a>` with `href`, `target`, `rel` |

### Graceful degradation

Unknown node types render their children inside a plain `<div>` — content survives even if your CMS uses custom nodes the renderer doesn't recognize.

If `doc` is a string (some CMSes store rendered HTML), it falls back to `dangerouslySetInnerHTML`. If `doc` is null, undefined, or not an object, the component renders nothing.

### Types

```ts
import type { RichTextNode, RichTextProps } from '@uidu/react';

// RichTextNode — the recursive Tiptap node shape: { type, content?, text?, marks?, attrs? }
```

***

## `<DynamicForm form action />`

Render any uidu `Form` schema as inputs, collect the values, and call your Server Action. Built on `useTransition`, no SWR or `react-hook-form` dependency. Used in the demo for contact forms, RSVPs, job applications, donations, and standalone form submissions — same component, different `action`.

```tsx
'use client';
import { DynamicForm } from '@uidu/react';
import { submitContactForm } from '@/lib/actions';

export function ContactForm({ form }) {
  return (
    <DynamicForm
      form={form}
      action={submitContactForm}
      submitLabel="Send message"
      renderSuccess={(result) => (
        <p>Thanks! Reference: <code>{String(result.meta?.responseId)}</code></p>
      )}
    />
  );
}
```

### Props

| Prop                   | Type                                                        | Default           | Description                                                                                     |
| ---------------------- | ----------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------- |
| `form`                 | `Form`                                                      | —                 | The schema from `getForm`, `call.form`, or an embedded page block.                              |
| `action`               | `(values: DynamicFormValues) => Promise<DynamicFormResult>` | —                 | Server Action wrapping `createFormResponse`. Receives parsed values, returns `{ ok, errors? }`. |
| `includeContactFields` | `boolean`                                                   | `true`            | Render first name, last name, and email above the form questions.                               |
| `submitLabel`          | `string`                                                    | `"Submit"`        | Submit button label.                                                                            |
| `submittingLabel`      | `string`                                                    | `"Submitting…"`   | Submit button label while the action is pending.                                                |
| `renderSuccess`        | `(result) => ReactNode`                                     | default green box | Custom success state.                                                                           |
| `classNames`           | `{ form?, input?, button?, … }`                             | sensible Tailwind | Per-element class overrides.                                                                    |

### Input type detection

The component inspects each `FormQuestion`'s `fieldKind` and `field.options` to pick the right HTML control:

| Signal                                               | Renders                   |
| ---------------------------------------------------- | ------------------------- |
| `field.options` is non-empty                         | `<select>`                |
| `fieldKind: 'checkbox'` or `'boolean'`               | `<input type="checkbox">` |
| `fieldKind: 'textarea'`, `'longtext'`, `'multiline'` | `<textarea>`              |
| `fieldKind: 'email'`                                 | `<input type="email">`    |
| `fieldKind: 'number'`, `'integer'`                   | `<input type="number">`   |
| `fieldKind: 'date'`                                  | `<input type="date">`     |
| `fieldKind: 'tel'`, `'phone'`                        | `<input type="tel">`      |
| `fieldKind: 'url'`                                   | `<input type="url">`      |
| anything else                                        | `<input type="text">`     |

Hidden questions (`hidden: true`) and system fields (`field.type === 'system'`) are filtered out automatically — the latter are handled by `includeContactFields`.

### The action contract

Your `action` receives parsed `DynamicFormValues`:

```ts
{
  contact?: { firstName, lastName, email },         // only if includeContactFields
  fieldValues: [{ fieldId, questionId, value }, …], // one per non-system question
  formData: FormData,                                // raw, in case you need extras
}
```

A typical `submitForm` Server Action looks like:

```ts
'use server';
import { createFormResponse } from '@uidu/client';
import { uidu } from './uidu';

export async function submitForm(formId, values) {
  const result = await createFormResponse(uidu, {
    input: {
      attributes: {
        formId,
        fieldValuesAttributes: values.fieldValues.map((fv) => ({
          fieldId: fv.fieldId,
          content: { value: fv.value },
        })),
        contactAttributes: values.contact && {
          email: values.contact.email,
          contactableAttributes: {
            kind: 'person',
            firstName: values.contact.firstName,
            lastName: values.contact.lastName || undefined,
          },
        },
      },
      event: 'complete!',
    },
  });
  return result?.errors?.length
    ? { ok: false as const, errors: result.errors }
    : { ok: true as const, meta: { responseId: result?.formResponse?.id } };
}
```

The same action shape works for contact forms, RSVPs (with a different mutation), donations (different mutation again), and job applications.

### Styling

Defaults are reasonable Tailwind classes. Override per element with the `classNames` prop:

```tsx
<DynamicForm
  form={form}
  action={action}
  classNames={{
    input: 'rounded-xl border-emerald-200 focus:border-emerald-500',
    button: 'rounded-xl bg-emerald-600 hover:bg-emerald-700',
  }}
/>
```

For deeper customization (custom Input components, field-level adornments, complex layout), drop down to rendering the questions yourself — `form.inputs.edges` gives you everything `<DynamicForm>` uses internally.

***

## Helper exports

```ts
import { getBlockShortname, type BlockComponent } from '@uidu/react';

// getBlockShortname(block) — extracts the canonical shortname from a PageBlock.
//   Tries block.templateBlock.block.shortname first, then block.shortname.
//
// BlockComponent — type alias for ComponentType<{ block: PageBlock }>.
//   Use it to type your own block component registries.
```

## Patterns

### Per-route component maps

Different pages can use different block components. Keep the registry close to the page:

```tsx
// app/blog/[slug]/page.tsx
const blogBlocks = {
  Header: BlogHero,
  Body: ProseBody,
  CTA: NewsletterSignup,
};

export default async function BlogPost({ params }) {
  const page = await getPage(uidu, { projectId, slug: params.slug });
  return <PageBlocks pageBlocks={page?.pageBlocks} components={blogBlocks} />;
}
```

### Shared registry across pages

For consistency, hoist the registry into `lib/`:

```ts
// lib/blocks.ts
export const siteBlocks = {
  Header: Hero,
  feature1: Feature,
  ModuloContatto: ContactForm,
  /* ... */
};
```

```tsx
// any page
import { siteBlocks } from '@/lib/blocks';
<PageBlocks pageBlocks={page.pageBlocks} components={siteBlocks} />
```
