agentsclimarketplace

Getting started

Skill hassanzohdy/mongez-react-form/skills/getting-started

@mongez/react-form — headless React form primitives. useFormControl, validation rules, form events, submit-button state. Web + React Native compatible. v4 rebuilt the form on a React-free FormEngine class exposed via ref, with SSR-safe ids, async validation that gates submission, Standard Schema (zod/valibot/seal) interop, reactive hydration, field arrays, and useWatch. This page covers install, locale registration for validation messages, and the minimal first form.From its SKILL.md

Install
npx -y skills add hassanzohdy/mongez-react-form --skill getting-started

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

13.5 KB, as published. Nobody here has run it

@mongez/react-form — Get started

Headless React form primitives. useFormControl wires any custom input into a typed form context; <Form> (Web) or <NativeForm> (React Native) collects values; validation rules emit localized error messages. No Formik ceremony, no react-hook-form ref management — just hooks that read and write to a shared form context.

v4 (major). Form and NativeForm are now function components (forwardRef) built over a plain, React-free FormEngine class — the engine instance is what a ref to the form gives you. Ids are SSR-safe via React's useId(). The old abstract BaseForm React component is now a deprecated alias for FormEngine (kept through v4, removed in v5). See migration notes at the end.

Highlighted features

<div class="mongez-highlights"> <div class="mongez-highlight" data-accent="ice"> <svg class="mongez-highlight-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg> <h3>Headless controls via <code>useFormControl</code></h3> <p>Build any input shape — text, checkbox, radio, multi-value — by reading <code>value</code>, <code>changeValue</code>, <code>error</code> from the hook. Your component stays in charge of rendering.</p> </div> <div class="mongez-highlight" data-accent="ice"> <svg class="mongez-highlight-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> <h3>Composable validation rules</h3> <p><code>rules: [requiredRule, emailRule]</code> — combine built-ins or write your own. Error messages render through <code>@mongez/localization</code> (six locales ship: en, ar, fr, es, it, de).</p> </div> <div class="mongez-highlight" data-accent="fire"> <svg class="mongez-highlight-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> <h3>Dot-notation names</h3> <p><code>name="user.firstName"</code> auto-nests into <code>values.user.firstName</code> on submit. No flat-key gymnastics for nested forms.</p> </div> <div class="mongez-highlight" data-accent="fire"> <svg class="mongez-highlight-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg> <h3>Smart submit-button state</h3> <p><code>useSubmitButton</code> exposes <code>isSubmitting</code>, <code>disabled</code>, <code>isDirty</code> — drop into your button without prop-drilling form state. (For the full snapshot use <code>useFormState()</code>.)</p> </div> <div class="mongez-highlight" data-accent="bolt"> <svg class="mongez-highlight-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg> <h3>Web + React Native</h3> <p><code>&lt;Form&gt;</code> renders a real HTML form; <code>&lt;NativeForm&gt;</code> renders a fragment and submits programmatically. Same hooks, same validation, same API.</p> </div> <div class="mongez-highlight" data-accent="bolt"> <svg class="mongez-highlight-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg> <h3>Form lifecycle events</h3> <p><code>onSubmit</code> / <code>onError</code> props, plus <code>form.on("change" | "submit" | "validControls" | "invalidControls" | "dirty" | "reset", …)</code> — wire side effects without rebuilding a context provider.</p> </div> <div class="mongez-highlight" data-accent="ice"> <svg class="mongez-highlight-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg> <h3>Async validation that gates submit</h3> <p>A rule may return a <code>Promise&lt;ReactNode&gt;</code> — the engine awaits it before submitting, while sync rules stay synchronous. <code>isValidating</code> exposes the in-flight state.</p> </div> <div class="mongez-highlight" data-accent="fire"> <svg class="mongez-highlight-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg> <h3>Standard Schema interop</h3> <p><code>&lt;Form schema={...}&gt;</code> validates the whole form on submit with zod / valibot / <code>@warlock.js/seal</code> — zero runtime dependency, with <code>onSubmit</code> values fully typed from the schema.</p> </div> </div>

Install

npm install @mongez/react-form
# or: yarn add @mongez/react-form
# or: pnpm add @mongez/react-form

Peer dep: react >= 18. Runtime deps install transitively: @mongez/events, @mongez/localization, @mongez/supportive-is, @mongez/reinforcements.

Quick peek

import { Form, useFormControl, requiredRule, emailRule } from "@mongez/react-form";

function TextInput(props) {
  const { value, changeValue, error, otherProps } =
    useFormControl({ ...props, rules: [requiredRule, emailRule] });
  return (
    <>
      <input value={value} onChange={(e) => changeValue(e.target.value)} {...otherProps} />
      {error && <span>{error}</span>}
    </>
  );
}

<Form onSubmit={({ values }) => api.signup(values)}>
  <TextInput name="email" type="email" required />
  <button type="submit">Sign up</button>
</Form>

Wrap your own input around useFormControl, drop it inside <Form>, get typed values on submit.

Full setup steps

1. Register validation translations (one-time, at app entry)

Validation rules emit error messages through @mongez/localization. The translation bundles must be registered under the validation namespace before any form mounts. Do this once at the root of the app (typically src/main.tsx or App.tsx):

import { extend } from "@mongez/localization";
import {
  enValidationTranslation,
  arValidationTranslation,
} from "@mongez/react-form";

extend("en", { validation: enValidationTranslation });
extend("ar", { validation: arValidationTranslation });

Six locales ship: en, ar, fr, es, it, de. Register only those you need.

If this step is skipped, validation still runs but errors appear as raw translation keys (e.g. validation.required) instead of human-readable text.

2. Pick the right form component

  • Web → import Form from @mongez/react-form. Renders an HTML <form> element. Submits via the standard browser submit event.
  • React Native → import NativeForm from @mongez/react-form. Renders a Fragment by default (no host element). Submission is always programmatic.

Both expose the same API — the only differences are the rendered output and how submit is triggered.

3. Minimal first form (Web)

import { Form, useFormControl, requiredRule, type FormControlProps } from "@mongez/react-form";

function TextInput(props: FormControlProps) {
  const { value, changeValue, id, error } = useFormControl({
    rules: [requiredRule],
    ...props,
  });

  return (
    <>
      <input id={id} value={value} onChange={(e) => changeValue(e.target.value)} />
      {error && <span style={{ color: "red" }}>{error}</span>}
    </>
  );
}

export default function App() {
  return (
    <Form onSubmit={({ values }) => console.log(values)}>
      <TextInput name="firstName" required />
      <TextInput name="lastName" />
      <button>Submit</button>
    </Form>
  );
}

The name prop on each TextInput becomes the key in the submitted values object. Dot notation (user.firstName) is supported and produces nested objects.

4. Verify the baseline

After completing steps 1–3, you should be able to:

  • Mount the form, type into both inputs, click Submit.
  • See the values object logged with both names.
  • Submit with an empty first name and see the localized "This input is required" error rendered inline.

If any of those fail, the likely cause is one of:

  • Validation translations not registered → errors show as validation.required text.
  • name prop missing on an input → it won't be collected into values.
  • <button> placed outside the <Form> → click won't trigger form submission.

5. SSR is safe by default (v4); pass a stable id when you need a deterministic one

v4 derives the <Form> wrapper id from React's useId() when you omit id, so the rendered <form id="form-<react-id>"> attribute matches between the server render and the client hydration — no hydration mismatch, no manual id required. (In v3 the wrapper fell back to Math.random(), which differed across the two passes; that's fixed.)

You still want an explicit, stable id whenever you need the form's identity to be predictable rather than auto-generated:

<Form id="signup" onSubmit={({ values }) => api.signup(values)}>
  <TextInput name="email" type="email" required />
  <SubmitButton>Sign up</SubmitButton>
</Form>

Rules of thumb:

  • Pass an id when you target the form in tests, styles, or analytics — useId() produces opaque values like form-:r3:, while id="signup" renders the stable form-signup.
  • Keep an explicit id unique on the page. The id drives the engine's event prefix (form.<id>), so two forms sharing an id would cross-wire their events.
  • Inputs never need an id. A control's id is derived from its name (input-<name>), already deterministic on both server and client.

6. The engine behind the form (FormEngine)

A ref to <Form> / <NativeForm> resolves to the FormEngine instance (typed as FormInterface) — a plain, React-free class that owns registration, validation, value collection, dirty tracking, events, hydration, and the submit pipeline:

import { Form, type FormInterface } from "@mongez/react-form";

const formRef = useRef<FormInterface>(null);

<Form ref={formRef} onSubmit={({ values }) => api.save(values)}>
  {/* ... */}
</Form>;

// later, imperatively:
formRef.current?.fill({ email: "[email protected]" });
formRef.current?.reset();

You rarely instantiate FormEngine yourself — the function components do it via useFormEngine. The deprecated BaseForm export is now just an alias for FormEngine; replace extends BaseForm usage with the function-component + engine pattern.

Migrating from v3

  • Form / NativeForm are function components now. If you held a ref, its type changed from the old BaseForm class instance to FormEngine (both satisfy FormInterface, so most code is unaffected).
  • BaseForm is deprecated. It re-exports FormEngine. Subclassing it as a React component no longer works — subclass FormEngine and render a thin function component (mirror useFormEngine) for custom renderers.
  • Drop manual id workarounds for SSR. The useId()-based default already fixes the v3 hydration mismatch.
  • form.submitting(false) is optional for awaited submits. If onSubmit returns a Promise, the engine clears the submitting state when it settles. See Submit button.
  • Everything else — useFormControl, rules, events, dot-notation names, useForm, useSubmitButton — keeps the same surface, plus new hooks (useFieldArray, useWatch) and new engine methods (fill, setValues, setErrors).

Where to go next

  • Create form control — patterns for text inputs, checkboxes, radios, multi-value controls, a11y prop bags, custom validation
  • Form events — change, submit, validation, dirty, reset lifecycle hooks
  • Submit button — useSubmitButton, smart submit state, awaited-submit auto-clear
  • Validation rules — built-in rules, async validation that gates submit, writing custom ones
  • Standard Schema validation — <Form schema>, per-field schema, type inference with zod / valibot / seal
  • Form hydration — values prop, form.fill / setValues / setErrors, defaultValue as reset baseline
  • Field arrays — useFieldArray for dynamic repeated rows
  • Watching values — useWatch for dependent fields and conditional UI
  • React Native usage — switching from Form to NativeForm
  • Recipes — common patterns

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.