npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@form-instant/react-input-mapping

v2.2.4

Published

<!-- Documentación empaquetada para consumo por IA. Generado desde README.md + views/*.md -->

Downloads

351

Readme

Quick start guide

1. Installation and dependency order

Install in this order (or all at once in a monorepo):

  1. React (peer)

    bun add react react-dom
    # or: npm i react react-dom
  2. Zod (for defining schemas)

    bun add zod
    # or: npm i zod
  3. @form-instant/react-input-mapping (field → component mapping)

    bun add @form-instant/react-input-mapping
    # or: npm i @form-instant/react-input-mapping
  4. @form-instant/react-resolver-zod (resolves Zod schema and provides FormInstantProvider / FormInstantElement)

    bun add @form-instant/react-resolver-zod
    # or: npm i @form-instant/react-resolver-zod

    This package has as peers: react, @form-instant/react-input-mapping, and zod. Without the mapping installed first, the resolver cannot resolve fields.

Summary: reactzod@form-instant/react-input-mapping@form-instant/react-resolver-zod.

<span id="implementation">

Implementation in your project

2.1 Creating the Mapping

The Mapping is a dictionary that associates each fieldType (string) with a React component. You can use InputMapping or InputMappingStore (recommended: better support for granular re-renders).

  • Default keys (INPUT_COMPONENTS_KEYS): 'checkbox' | 'date' | 'select' | 'radio' | 'switch' | 'textarea' | 'number' | 'file' | 'text' | 'fallback'.
  • Extra keys: you can extend with your own types (e.g. email, password, customSelect).
  • Each component receives ParsedField: name (current, history), fieldType, required, default, fieldConfig, and for selects/enums options as [value, label][].
  • Important: Type your components as FC<ParsedField<MyInputs['key']>>, not FC<FieldConfig<...>>. At runtime, ElementMapping passes a full ParsedField object. FieldConfig only describes the config shape per field; ParsedField is what each component actually receives.

Minimal example (in a file like providers/input-mapping.tsx):

import {
  InputMappingStore,
  createFormInstantContainer,
  type ParsedField,
  type FieldConfig,
} from '@form-instant/react-input-mapping';
import type { FC } from 'react';

export type MyInputs = {
  text: FieldConfig<{ placeholder?: string; label?: string }>;
  number: FieldConfig<{ placeholder?: string; label?: string; min?: number; max?: number }>;
  textarea: FieldConfig<{ placeholder?: string; label?: string }>;
  date: FieldConfig;
  email: FieldConfig<{ placeholder?: string; label?: string }>;
  password: FieldConfig<{ placeholder?: string; label?: string }>;
  checkbox: FieldConfig;
  select: FieldConfig;
  fallback: FieldConfig;
};

const TextInput: FC<ParsedField<MyInputs['text']>> = ({ name, fieldConfig, required, ...props }) => (
  <div>
    <label htmlFor={name.current}>{(fieldConfig as any)?.label ?? name.current}{required && ' *'}</label>
    <input id={name.current} name={name.history} type="text" required={required} {...props} />
  </div>
);

// Define the rest: NumberInput, TextareaInput, DateInput, EmailInput, PasswordInput,
// CheckboxInput, SelectInput (uses props.options as [value, label][]), FallbackInput...

export const inputMapping = new InputMappingStore<MyInputs>({
  text: TextInput,
  number: NumberInput,
  textarea: TextareaInput,
  date: DateInput,
  email: EmailInput,
  password: PasswordInput,
  checkbox: CheckboxInput,
  select: SelectInput,
  fallback: FallbackInput,
});

export const { FormInstantInputsProvider, useInputMapping } =
  createFormInstantContainer<MyInputs>(inputMapping);
  • Select: the component receives options?: [string, string][]; you can populate them from an API (data fetch) and keep using the same mapping.

2.2 Setting up providers

  • createFormInstantContainer<MyInputs>(inputMapping) returns:
    • FormInstantInputsProvider: provider that should wrap your app (or at least the area where forms are used).
    • useInputMapping: hook to access the mapping (e.g. for custom components that use ElementMapping).

Place it at the root (e.g. App.tsx or layout.tsx):

import { FormInstantInputsProvider } from './providers/input-mapping';

function App() {
  return (
    <FormInstantInputsProvider>
      {/* routes, forms, etc. */}
    </FormInstantInputsProvider>
  );
}

2.3 Zod schema and fieldConfig

  • Define the schema with Zod. With @form-instant/react-resolver-zod the package already extends Zod with .fieldConfig() on the types used by the parser.
  • .fieldConfig(...) is used to specify fieldType and extra props (placeholder, label, min, max, etc.) that reach the component via fieldConfig.

Example:

import { z } from 'zod';
// If you use @form-instant/react-resolver-zod, you don't need to call extendZodWithFieldConfig:
// the module applies the extension on import.

const formSchema = z.object({
  name: z.string().min(2).fieldConfig({ fieldType: 'text', placeholder: 'Name', label: 'Name' }),
  email: z.email().fieldConfig({ fieldType: 'email', placeholder: '[email protected]' }),
  age: z.number().min(18).max(100).fieldConfig({ fieldType: 'number', min: 18, max: 100 }),
  bio: z.string().min(10).fieldConfig({ fieldType: 'textarea' }),
  role: z.enum(['admin', 'user']).fieldConfig({ fieldType: 'select' }),
});

export type FormSchemaType = z.infer<typeof formSchema>;
  • If you omit fieldConfig, the resolver infers fieldType from the Zod type (string → text, number → number, etc.).

2.4 Rendering forms

  • Wrap the form in FormInstantProvider with the schema. Pass a stable schema reference (e.g. defined outside the component or memoized with useMemo) so the provider does not re-parse the schema on every parent re-render.
  • Use FormInstantElement<FormSchemaType> name="..." for each path in the schema you want to render:
    • Primitive field: name="name" → a single input.
    • Nested object: name="personalData" → all fields of personalData are rendered (the provider parses the schema and exposes field.schema; FormInstantElement iterates and uses ElementMapping for each child).

Minimal example:

import { FormInstantProvider, FormInstantElement } from '@form-instant/react-resolver-zod';
import { formSchema, type FormSchemaType } from './schema';

export function MyForm() {
  return (
    <form onSubmit={...}>
      <FormInstantProvider schema={formSchema}>
        <FormInstantElement<FormSchemaType> name="name" />
        <FormInstantElement<FormSchemaType> name="email" />
        <FormInstantElement<FormSchemaType> name="age" />
        <FormInstantElement<FormSchemaType> name="bio" />
        <FormInstantElement<FormSchemaType> name="role" />
      </FormInstantProvider>
    </form>
  );
}

For nested objects, one FormInstantElement per object:

<FormInstantProvider schema={objectFormSchema}>
  <FormInstantElement<ObjectFormType> name="personalData" />
  <FormInstantElement<ObjectFormType> name="address" />
</FormInstantProvider>

<span id="form-types">

Form types

3.1 Static form

Flat schema, fixed fields. You only need FormInstantProvider plus several FormInstantElement (one per field or per object group).

const schema = z.object({
  name: z.string().min(2),
  email: z.email(),
  age: z.number().min(18).max(100),
});
// ...
<FormInstantProvider schema={schema}>
  <FormInstantElement<FormType> name="name" />
  <FormInstantElement<FormType> name="email" />
  <FormInstantElement<FormType> name="age" />
</FormInstantProvider>

3.2 Inputs with data fetch (select / autocomplete)

  • Select: in the schema use z.enum([...]) or a type the parser turns into options; the mapping component receives options and can also receive data loaded from an API (store options in state and pass them via context or props to the component that renders the field).
  • Autocomplete: you can define your own fieldType (e.g. autocomplete), map it to a component that uses useFields and internally fetches and shows suggestions; the final value is still bound to the same name.

The select mapping already uses options?: [string, string][]; populating options from an API is compatible with the same flow.

3.3 Dynamic form with discriminatedUnion

Schema that changes based on a discriminator (e.g. user type). Steps:

  1. Define the schema with z.discriminatedUnion('status', [ z.object({ status: z.literal('ok'), ... }), z.object({ status: z.literal('not'), ... }) ]).
  2. Use useSchema from @form-instant/react-resolver-zod: it receives a callback that returns the schema and a dependencies object; when the reference of the dependencies object changes, the schema and initial values are recalculated. Pass a stable dependencies object (e.g. from state or useMemo) to avoid unnecessary recalculations on parent re-renders.
  3. Keep dependencies in sync with the current discriminator value in the form (e.g. with form.watch('data.status') if you use react-hook-form, or your own state).
  4. The first field in the union is the discriminator; you can map that field to a select in your mapping (by fieldType or by the discriminator key) so the user can switch the variant.

Structure example (submit and form provider depend on your form library):

const formSchema = z.object({
  data: z.discriminatedUnion('status', [
    z.object({ status: z.literal('ok'), code: z.string() }),
    z.object({ status: z.literal('not'), birthday: z.coerce.date() }),
  ]),
});

const [dependencies, setDependencies] = useState({ status: 'ok' });
const { schema } = useSchema(() => formSchema, dependencies);

// In a useEffect or in the same flow as the form: when data.status changes,
// update setDependencies({ status: newStatus }) so useSchema returns
// the correct schema and FormInstantElement shows the fields for that variant.

<FormInstantProvider schema={schema}>
  <FormInstantElement<FormType> name="data" />
</FormInstantProvider>

3.4 Array with dynamic input creation

For arrays of objects (e.g. list of items or skills):

  1. Schema with z.array(z.object({ ... })) (optionally .min()/.max()).
  2. In the array component:
    • useFields({ key: 'items' }) (or the array field name) to get the parsed field.
    • useInputArray(field) (from @form-instant/react-input-mapping): returns inputs, append, remove, fieldConfig, etc.
  3. Render each item: for each entry in inputs, iterate over properties and use ElementMapping formProps={...} for each. +/- buttons that call append() and remove(index).

Condensed example:

const arrayFormSchema = z.object({
  items: z.array(z.object({
    name: z.string().min(2),
    quantity: z.number().min(1),
    price: z.number().min(0),
  })).min(1).max(10),
});

function ArrayFieldComponent({ name }: { name: 'items' }) {
  const field = useFields({ key: name });
  const { inputs, append, remove, fieldConfig } = useInputArray(field);
  const id = useId();

  return (
    <div>
      {inputs.map((inputFields, index) => (
        <div key={`${id}-${index}`}>
          {Object.entries(inputFields).map(([key, value]) => (
            <ElementMapping key={key} formProps={value} />
          ))}
          <button type="button" onClick={() => remove(index)}>Remove</button>
        </div>
      ))}
      <button type="button" onClick={append}>+ Add</button>
    </div>
  );
}

<FormInstantProvider schema={arrayFormSchema}>
  <ArrayFieldComponent name="items" />
</FormInstantProvider>

fieldConfig?.min / fieldConfig?.max can come from the schema (e.g. via .fieldConfig({ min: 1, max: 10 })) to disable buttons based on limits.

3.5 Nested object

Schema with z.object({ group: z.object({ a: z.string(), b: z.number() }) }). No custom component needed: a single FormInstantElement<FormType> name="group" makes the resolver render all fields of group (the parser fills field.schema and FormInstantElement walks those children with ElementMapping).

const objectFormSchema = z.object({
  personalData: z.object({
    firstName: z.string().min(2),
    lastName: z.string().min(2),
    email: z.string().email(),
  }),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zipCode: z.string(),
  }),
});

<FormInstantProvider schema={objectFormSchema}>
  <FormInstantElement<ObjectFormType> name="personalData" />
  <FormInstantElement<ObjectFormType> name="address" />
</FormInstantProvider>

<span id="full-example">

Full example: React Hook Form + Shadcn UI + Zod

This example wires react-hook-form, @hookform/resolvers (Zod resolver), Zod, and shadcn/ui (or any similar UI primitives) with Form Instant. Validation runs via zodResolver; inputs are rendered from your schema and mapping.

Dependencies

bun add react-hook-form @hookform/resolvers zod
# or: npm i react-hook-form @hookform/resolvers zod

Add shadcn/ui with your stack (e.g. npx shadcn@latest init) and install the components you need (e.g. Input, Label, Button). You can also use plain HTML elements or your own design system.

1. Zod schema

// lib/schemas/profile.ts
import { z } from 'zod';

export const profileSchema = z.object({
  username: z.string().min(2, 'At least 2 characters').fieldConfig({ fieldType: 'text', placeholder: 'Username', label: 'Username' }),
  email: z.string().email('Invalid email').fieldConfig({ fieldType: 'email', placeholder: '[email protected]', label: 'Email' }),
  age: z.number().min(18, 'Must be 18+').max(120).fieldConfig({ fieldType: 'number', label: 'Age', min: 18, max: 120 }),
  bio: z.string().min(10, 'At least 10 characters').fieldConfig({ fieldType: 'textarea', label: 'Bio', placeholder: 'Tell us about yourself' }),
});

export type ProfileFormValues = z.infer<typeof profileSchema>;

2. Input mapping (with react-hook-form registration)

Type each mapping component as FC<ParsedField<MyInputs['key']>>—they receive a full ParsedField, not FieldConfig alone.

For react-hook-form to validate and submit correctly, each rendered input must be registered. Use useFormContext() inside your mapping components and spread register(name.history) (and optionally show errors from formState.errors).

// components/input-mapping.tsx
import {
  InputMappingStore,
  createFormInstantContainer,
  type ParsedField,
  type FieldConfig,
} from '@form-instant/react-input-mapping';
import { useFormContext } from 'react-hook-form';
import type { FC } from 'react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';

export type MyInputs = {
  text: FieldConfig<{ placeholder?: string; label?: string }>;
  number: FieldConfig<{ placeholder?: string; label?: string; min?: number; max?: number }>;
  textarea: FieldConfig<{ placeholder?: string; label?: string }>;
  email: FieldConfig<{ placeholder?: string; label?: string }>;
  fallback: FieldConfig;
};

const TextInput: FC<ParsedField<MyInputs['text']>> = ({ name, fieldConfig, required, ...props }) => {
  const { register, formState: { errors } } = useFormContext();
  const label = (fieldConfig as any)?.label ?? name.current;
  const placeholder = (fieldConfig as any)?.placeholder ?? '';

  return (
    <div className="space-y-2">
      <Label htmlFor={name.current}>{label}{required && ' *'}</Label>
      <Input
        id={name.current}
        type="text"
        placeholder={placeholder}
        aria-invalid={!!errors[name.history]}
        {...register(name.history)}
        {...props}
      />
      {errors[name.history]?.message && (
        <p className="text-sm text-destructive">{String(errors[name.history]?.message)}</p>
      )}
    </div>
  );
};

const NumberInput: FC<ParsedField<MyInputs['number']>> = ({ name, fieldConfig, required, ...props }) => {
  const { register, formState: { errors } } = useFormContext();
  const label = (fieldConfig as any)?.label ?? name.current;

  return (
    <div className="space-y-2">
      <Label htmlFor={name.current}>{label}{required && ' *'}</Label>
      <Input
        id={name.current}
        type="number"
        min={fieldConfig?.min}
        max={fieldConfig?.max}
        aria-invalid={!!errors[name.history]}
        {...register(name.history, { valueAsNumber: true })}
        {...props}
      />
      {errors[name.history]?.message && (
        <p className="text-sm text-destructive">{String(errors[name.history]?.message)}</p>
      )}
    </div>
  );
};

const TextareaInput: FC<ParsedField<MyInputs['textarea']>> = ({ name, fieldConfig, required, ...props }) => {
  const { register, formState: { errors } } = useFormContext();
  const label = (fieldConfig as any)?.label ?? name.current;
  const placeholder = (fieldConfig as any)?.placeholder ?? '';

  return (
    <div className="space-y-2">
      <Label htmlFor={name.current}>{label}{required && ' *'}</Label>
      <Textarea
        id={name.current}
        placeholder={placeholder}
        aria-invalid={!!errors[name.history]}
        {...register(name.history)}
        {...props}
      />
      {errors[name.history]?.message && (
        <p className="text-sm text-destructive">{String(errors[name.history]?.message)}</p>
      )}
    </div>
  );
};

const EmailInput: FC<ParsedField<MyInputs['email']>> = ({ name, fieldConfig, required, ...props }) => {
  const { register, formState: { errors } } = useFormContext();
  const label = (fieldConfig as any)?.label ?? name.current;
  const placeholder = (fieldConfig as any)?.placeholder ?? '';

  return (
    <div className="space-y-2">
      <Label htmlFor={name.current}>{label}{required && ' *'}</Label>
      <Input
        id={name.current}
        type="email"
        placeholder={placeholder}
        aria-invalid={!!errors[name.history]}
        {...register(name.history)}
        {...props}
      />
      {errors[name.history]?.message && (
        <p className="text-sm text-destructive">{String(errors[name.history]?.message)}</p>
      )}
    </div>
  );
};

const FallbackInput: FC<ParsedField<MyInputs['fallback']>> = ({ name, fieldConfig, required, ...props }) => {
  const { register, formState: { errors } } = useFormContext();
  const label = (fieldConfig as any)?.label ?? name.current;

  return (
    <div className="space-y-2">
      <Label htmlFor={name.current}>{label}{required && ' *'}</Label>
      <Input
        id={name.current}
        type="text"
        aria-invalid={!!errors[name.history]}
        {...register(name.history)}
        {...props}
      />
      {errors[name.history]?.message && (
        <p className="text-sm text-destructive">{String(errors[name.history]?.message)}</p>
      )}
    </div>
  );
};

export const inputMapping = new InputMappingStore<MyInputs>({
  text: TextInput,
  number: NumberInput,
  textarea: TextareaInput,
  email: EmailInput,
  fallback: FallbackInput,
});

export const { FormInstantInputsProvider, useInputMapping } =
  createFormInstantContainer<MyInputs>(inputMapping);

3. Form component

Use FormProvider from react-hook-form, zodResolver with your schema, and FormInstantProvider + FormInstantElement so fields are rendered from the schema and your mapping.

// components/profile-form.tsx
import { useForm, FormProvider } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { FormInstantProvider, FormInstantElement } from '@form-instant/react-resolver-zod';
import { profileSchema, type ProfileFormValues } from '@/lib/schemas/profile';
import { Button } from '@/components/ui/button';

export function ProfileForm() {
  const form = useForm<ProfileFormValues>({
    resolver: zodResolver(profileSchema),
    defaultValues: {
      username: '',
      email: '',
      age: undefined,
      bio: '',
    },
  });

  const onSubmit = (data: ProfileFormValues) => {
    console.log(data);
  };

  return (
    <FormProvider {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
        <FormInstantProvider schema={profileSchema}>
          <FormInstantElement<ProfileFormValues> name="username" />
          <FormInstantElement<ProfileFormValues> name="email" />
          <FormInstantElement<ProfileFormValues> name="age" />
          <FormInstantElement<ProfileFormValues> name="bio" />
        </FormInstantProvider>
        <Button type="submit">Submit</Button>
      </form>
    </FormProvider>
  );
}

4. App root

Ensure FormInstantInputsProvider wraps the tree where forms use the mapping (e.g. root layout or App).

// App.tsx
import { FormInstantInputsProvider } from '@/components/input-mapping';
import { ProfileForm } from '@/components/profile-form';

export default function App() {
  return (
    <FormInstantInputsProvider>
      <ProfileForm />
    </FormInstantInputsProvider>
  );
}

Summary: Use Zod for the schema, zodResolver in useForm, FormProvider (react-hook-form) around the form, and FormInstantProvider + FormInstantElement to render fields from the schema. In your mapping components, call useFormContext() and register(name.history) (and optionally formState.errors) so react-hook-form controls and validates the inputs.

<span id="api-reference">

API reference (minimal)

| Concept | Package | Description | | -------------------------------------------------------------- | -------------------------------- | --------------------------------------------------------------------- | | InputMapping / InputMappingStore | react-input-mapping | Maps fieldType → React component. | | createFormInstantContainer | react-input-mapping | Creates FormInstantInputsProvider and useInputMapping. | | ParsedField, FieldConfig | react-input-mapping | Props types for mapping components. | | ElementMapping | react-input-mapping | Renders a single field from formProps (fieldType, name, etc.). | | useInputArray | react-input-mapping | For arrays:inputs, append, remove, fieldConfig. | | FormInstantProvider, FormInstantElement, useFields | react-resolver-zod | Schema provider, element per path, hook for a field. | | useSchema | react-resolver-zod | Reactive schema (e.g. for discriminatedUnion) and initial values. Recalculates when the reference of the dependencies object changes; pass a stable object to avoid unnecessary recalculations. | | .fieldConfig(...) | react-resolver-zod (extends Zod) | Associate fieldType and props with a schema field. |