Build a preference form with explicit state, save progress, and retry feedback.
This tutorial adds one task to an existing React application: change an email preference and save it. Liquidify supplies the controls; React state and the application callback own the draft and the save result.
Use React 19 and React DOM 19 with working Liquidify implementation packages. Read package availability first: the checked npm placeholder cannot run this example.
Use the existing framework guides for CSS placement and rendering boundaries. In Next.js App Router, place the interactive example in a client module with "use client" before imports and load the stylesheet from the application layout. Define the ordinary onSave function inside that client boundary; do not pass it across a server boundary.
Add the following to a TypeScript React module and render its exported App from your application's entry point. If the application already imports the aggregate stylesheet at its root, keep that single import there.
import { useId, useRef, useState } from "react"
import type { FormEvent } from "react"
import { Button, Theme, Toggle } from "@liquidify/react"
import "@liquidify/react/styles.css"
type PreferencesFormProps = {
initialEnabled: boolean
onSave: (enabled: boolean) => Promise<void>
}
export function PreferencesForm({ initialEnabled, onSave }: PreferencesFormProps) {
const labelId = useId()
const [enabled, setEnabled] = useState(initialEnabled)
const [status, setStatus] = useState<"idle" | "saving" | "saved" | "error">("idle")
const inFlight = useRef(false)
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
if (inFlight.current) return
inFlight.current = true
setStatus("saving")
try {
await onSave(enabled)
setStatus("saved")
} catch {
setStatus("error")
} finally {
inFlight.current = false
}
}
return (
<form onSubmit={submit} aria-label="Notification preferences">
<p id={labelId}>Email notifications</p>
<Toggle
aria-labelledby={labelId}
checked={enabled}
disabled={status === "saving"}
onChange={(next) => {
setEnabled(next)
setStatus("idle")
}}
/>
<p>Choose whether to receive email notifications, then save your preference.</p>
<Button type="submit" variant="borderedProminent" loading={status === "saving"}>
Save preferences
</Button>
<p role="status">
{status === "saving" ? "Saving preferences…" : status === "saved" ? "Preferences saved." : ""}
</p>
{status === "error" ? <p role="alert">Could not save preferences. Your choice is still here. Try again.</p> : null}
</form>
)
}
// Local demonstration only. Replace onSave with your application's persistence.
export function App() {
const [saved, setSaved] = useState(false)
return (
<Theme type="system">
<PreferencesForm
initialEnabled={false}
onSave={async (next) => {
await new Promise<void>((resolve) => setTimeout(resolve, 600))
setSaved(next)
}}
/>
<p>Saved in this demo: {saved ? "enabled" : "disabled"}. Refreshing clears it.</p>
</Theme>
)
}The demonstration saves only in React memory. Replace onSave with the operation that persists your product's preference. Resolve its promise only after a successful save and reject it when the operation fails; for an HTTP request, check unsuccessful response statuses explicitly.
checked is the current draft and onChange supplies the next boolean. initialEnabled seeds the draft when the form mounts; it does not continually synchronize with props. Load the initial value before mounting, or remount deliberately when switching records.
The native form owns submission through onSubmit. The Button has type="submit" and no second save handler. Its loading state suppresses activation while keeping it focusable. The application also guards submission with a ref so repeated submissions cannot start another save.
While saving, the Toggle is disabled to keep the submitted value stable. Failure preserves that value and allows retry. Editing after success clears the previous success message. Changing the draft does not save it automatically.
Figure in preparation
A preference through its save states
Four captures of the tutorial form: draft, saving, failure, and success.
Keep progress and outcome feedback near the task. The controls do not provide a backend, automatic retries, or durable storage.