TreegeRenderer
The TreegeRenderer component transforms workflow into interactive forms with built-in validation and security features.
Import
import { TreegeRenderer } from "treege/renderer"For React Native applications, use:
import { TreegeRenderer } from "treege/renderer-native"See the React Native Guide for detailed instructions.
Props
flow (optional)
The flow data to render as a form.
flow?: Flow | null- If a
Flow: renders that flow as a form - If
nullorundefined: renders nothing
onSubmit (optional)
Callback function called when the form is submitted.
onSubmit?: (values: FormValues, meta?: Meta) => voidParameters:
values: Form values (keyed by field name or node ID)meta: Optional metadata about the submission (e.g., HTTP response data)
onChange (optional)
Callback called whenever form values change.
onChange?: (values: FormValues) => voidextraPayload (optional)
Extra data, owned by the host app rather than the form, attached to every submission — e.g. the logged-in user id or a tenant id. The fields are merged at the top level of both the onSubmit payload and the body of the built-in HTTP submit, so they ride along wherever the form data goes.
extraPayload?: Record<string, unknown> | ((values: FormValues) => Record<string, unknown>)Pass a static object for a value already known at render time:
<TreegeRenderer flow={flow} extraPayload={{ userId }} onSubmit={handleSubmit} />
// onSubmit receives → { ...formValues, userId }Pass a function when the value must be read at submit time rather than captured at render — a rotating auth token, a timestamp, or a value derived from the current answers. It receives the same name-keyed values you get in onSubmit:
<TreegeRenderer flow={flow} extraPayload={() => ({ token: auth.getToken() })} />
<TreegeRenderer flow={flow} extraPayload={(values) => ({ fullName: `${values.firstName} ${values.lastName}` })} />Notes:
- The extra fields are spread last, so a key here intentionally overrides a same-named form field.
- For the built-in HTTP submit (
submitConfig), the fields are merged into the request body, respecting anypayloadTemplateshape. If the config would otherwise send no body, the extra is sent on its own — your data is never silently dropped. - A function that returns a non-object (array / primitive) is ignored.
theme (optional)
Theme for the rendered form.
theme?: "light" | "dark"Default: "dark"
language (optional)
Current language for translations.
language?: stringDefault: "en"
validationMode (optional)
When to trigger validation.
validationMode?: "onSubmit" | "onChange"Default: "onSubmit"
initialValues (optional)
Initial values for the form fields — use this to pre-fill the form when editing a record that was already submitted.
initialValues?: FormValuesKeys can be either node.id OR the same name-based keys you receive back in onChange / onSubmit, so the previously-submitted object can be fed straight back in without any remapping:
const handleSubmit = (values) => save(values) // e.g. { firstName: "Alice" }
// later, to edit:
<TreegeRenderer flow={flow} initialValues={savedRecord} /> // same object works as-isReactive: if initialValues changes after mount (e.g. an async-fetched record resolves later), the form is re-seeded automatically — no need for a key prop or isLoading gate. A new object literal of identical content does not reset the form, so in-progress user edits are preserved.
const { data } = useQuery(/* ... */) // resolves asynchronously
<TreegeRenderer flow={flow} initialValues={data ?? {}} /> // fills in once data arrivesNote: This prop was previously named defaultValues in earlier versions.
Pre-filling file fields: A file field's value is a serializable object (never a DOM File, so it round-trips through JSON) — a single object, an array when the field is multiple, or null:
type SerializableFile = {
name: string;
size: number;
type: string;
lastModified: number;
data: string; // base64 data-URL (web) or file URI (native)
};Pass the same shape in initialValues to pre-fill a file field when editing — the renderer lists the files and lets the user remove them or add more. Only name / size are used for display, so data can hold a server URL for already-uploaded files:
<TreegeRenderer
flow={flow}
initialValues={{
attachment: { name: "contract.pdf", size: 24576, type: "application/pdf", lastModified: 0, data: "https://cdn.example.com/contract.pdf" },
}}
/>components (optional)
Custom component renderers. Override any of: inputs, inputLabel (the shared field-label component), ui, form, step, submitButton, submitButtonWrapper, loadingSkeleton.
components?: TreegeRendererComponentsSee Customization for details.
validate (optional)
Custom validation function.
validate?: (values: FormValues, nodes: Node<TreegeNodeData>[]) => Record<string, string>Parameters:
values: Current form valuesnodes: All visible nodes in the tree
Returns: Object mapping node IDs to error messages
googleApiKey (optional)
Google Maps API key for address autocomplete.
googleApiKey?: stringIf not provided, falls back to free Nominatim (OpenStreetMap).
headers (optional)
Global HTTP headers applied to every request issued by the renderer (HTTP inputs, submit buttons). Field-level headers with the same key (case-insensitive) take precedence.
headers?: HttpHeaders // Record<string, string>, e.g. { Authorization: "Bearer ..." }isLoading (optional)
When true, renders a loading skeleton instead of the form — useful while the flow is being fetched. Customize the skeleton via components.loadingSkeleton.
isLoading?: booleanDefault: false
<TreegeRenderer flow={flow ?? null} isLoading={isPending} onSubmit={handleSubmit} />isSubmitting (optional)
When true, forces the submit/continue button into its loading state (spinner + disabled). Use this to keep the button busy while an async onSubmit is still resolving on your side.
isSubmitting?: booleanDefault: false
It is OR-ed with the renderer's own internal submitting state (e.g. during an HTTP submitConfig call), so it only ever adds to the built-in behavior.
<TreegeRenderer flow={flow} isSubmitting={mutation.isPending} onSubmit={handleSubmit} />formId (optional)
Sets the id attribute on the underlying <form> element, so a submit button rendered outside the renderer can target it via the native HTML form attribute. Web only.
formId?: string<TreegeRenderer flow={flow} formId="my-form" onSubmit={handleSubmit} />
// anywhere else on the page — even outside the renderer:
<button type="submit" form="my-form">Save</button>In a multi-step flow this external button (and pressing Enter in a field) only triggers the real submission on the final step (path complete) — on earlier steps it advances to the next step, mirroring the built-in Continue button.
onBack (optional)
Called when the user clicks Back on the first step. Provide this to bridge back-navigation to an outer flow — e.g. stepping back in a parent modal that embeds the renderer.
onBack?: () => voidWhen set, a Back button is shown on the first step and triggers this callback instead of being a no-op. It has no effect on later steps, which always navigate back internally.
<TreegeRenderer flow={flow} onBack={() => modal.goToPreviousStep()} onSubmit={handleSubmit} />title (optional)
Form title rendered above the form. Works on both web and React Native.
title?: string | TranslatableAccepts either a plain string or a Translatable object (keyed by language code), resolved with the current language:
<TreegeRenderer flow={flow} title="Contact form" />
// Multi-language
<TreegeRenderer
flow={flow}
title={{ en: "Contact form", fr: "Formulaire de contact" }}
language="fr"
/>className (optional)
Additional CSS class names applied to the renderer container.
className?: stringstyle (optional)
Inline styles applied to the renderer's root container, alongside className. Web only.
style?: CSSProperties<TreegeRenderer flow={flow} style={{ maxWidth: 480 }} />disableSectionBorder (optional)
Removes the default border of the <section> wrapping each step — padding and layout are kept, only the border disappears. Useful when the renderer is embedded in a container that already has its own frame (card, modal…). Web only.
disableSectionBorder?: boolean<TreegeRenderer flow={flow} disableSectionBorder />Multi-Step Forms
When a flow contains Group nodes, the renderer automatically splits the form into navigable steps with Back/Continue controls. You can override the step layout via components.step — see Customization.
Continue becomes Submit only on the final step — when the path is actually complete (end of path reached, or an explicit submit input is visible). On a branching boundary step (the last step currently visible while the flow still continues behind an unanswered choice), the button stays a disabled Continue, and pressing Enter never submits — an incomplete flow can't be sent.
Auto-advance: when a step's only interactive field is a single-choice input (radio, non-multiple select, autocomplete, or an HTTP input mapped to options), selecting an option automatically advances to the next step after a short delay (~180 ms). It only triggers on a direct user selection — never on mount, when navigating back to an answered step, or on the final step (a choice never submits silently).
Two props help integrate multi-step flows into a surrounding app:
onBack— bridges the Back button on the first step to an outer flow (e.g. a parent modal). See theonBackprop.formId— lets a submit button rendered outside the renderer drive submission; in a multi-step flow it only submits on the final step, advancing otherwise. See theformIdprop.
Headless Usage
For full programmatic control, use the useTreegeRenderer hook directly. It accepts the same configuration as TreegeRenderer and returns the form state and control methods (formValues, setFieldValue, handleSubmit, step navigation, etc.):
import { useTreegeRenderer } from "treege/renderer"
const { formValues, setFieldValue, handleSubmit, isSubmitting } = useTreegeRenderer({
flow: flow,
onSubmit: (values) => console.log(values),
})The hook also exposes the full step machinery, so a custom UI gets the exact same behavior as the built-in renderers:
handleContinue/handleBack— ready-made action handlers (Continue submits only on the final step; Back bridges to theonBackprop on the first step).canContinue/canGoBack— button gating (required fields filled, a next step or a complete path exists).isFinalStep—trueonly when the path is complete; drives the Continue vs Submit label. UnlikeisLastStep, it staysfalseon a branching boundary step.stepLabel/formTitle— translated label of the current group and of thetitleprop.
The return type is exported as UseTreegeRendererReturn.
Global Configuration
Wrap your app with TreegeRendererProvider to define default options shared by every TreegeRenderer below it — instead of repeating the same props on each instance. It accepts the same configuration fields as TreegeRenderer (language, theme, googleApiKey, headers, baseUrl, validationMode, components):
import { TreegeRendererProvider, TreegeRenderer } from "treege/renderer"
function App() {
return (
<TreegeRendererProvider
language="fr"
theme="light"
googleApiKey="your-google-api-key"
>
{/* Every renderer below inherits the provider's config */}
<TreegeRenderer flow={flowA} onSubmit={handleA} />
<TreegeRenderer flow={flowB} onSubmit={handleB} />
</TreegeRendererProvider>
)
}Props always take precedence over the provider, which takes precedence over the built-in defaults (language: "en", theme: "dark", validationMode: "onSubmit"). So you can set an app-wide default and override it on a single instance:
<TreegeRendererProvider language="fr">
<TreegeRenderer flow={flow} /> {/* renders in French */}
<TreegeRenderer flow={flow} language="en" /> {/* overridden to English */}
</TreegeRendererProvider>components.inputs / components.ui are merged field-by-field (provider + instance), and headers from the provider and the instance are combined — so a renderer can add or override a single entry without losing the shared ones.
The provider is optional: used outside it, TreegeRenderer falls back to its props and the built-in defaults. You can also read the resolved global config with the useTreegeRendererConfig hook.
Security Features
XSS Protection
TreegeRenderer includes built-in protection against Cross-Site Scripting (XSS) attacks using DOMPurify. All user input is automatically sanitized before being rendered to prevent malicious code injection.
Key Features:
- Automatic HTML sanitization for all text inputs
- Protection against script injection
- Safe rendering of user-generated content
- No configuration required - works out of the box
This ensures that even if users input malicious HTML or JavaScript code, it will be safely sanitized before rendering.
// User input with potential XSS
const maliciousInput = "<script>alert('XSS')</script>";
// TreegeRenderer automatically sanitizes this
<TreegeRenderer
flow={flow}
initialValues={{ comment: maliciousInput }}
/>
// Output: Safe, sanitized text without the script tagExample
Basic Usage
<TreegeRenderer
flow={flow}
theme="dark"
language="en"
validationMode="onSubmit"
onSubmit={(values, meta) => {
console.log("Form values:", values)
console.log("Metadata:", meta)
}}
onChange={(values) => setFormValues(values)}
initialValues={{ name: "John", email: "john@example.com" }}
/>With Custom Components and Validation
<TreegeRenderer
flow={flow}
theme="dark"
onSubmit={(values) => console.log(values)}
components={{
inputs: {
text: CustomTextInput,
email: CustomEmailInput,
},
submitButton: ({ label }) => (
<button type="submit" className="btn-primary">
{label || "Submit"}
</button>
),
}}
validate={(values, nodes) => {
const errors: Record<string, string> = {}
if (values.age && values.age < 18) {
errors.age = "Must be 18 or older"
}
return errors
}}
googleApiKey={process.env.GOOGLE_MAPS_API_KEY}
/>