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

react-hook-native-forms

v0.1.0

Published

React Native form components built on react-hook-form

Readme

react-hook-native-forms

npm version CI License: MIT

A small, React Native–first form layer built on top of react-hook-form. Instead of wiring Controller / useController yourself, you compose two components:

// The app owns the form instance; <Form> only publishes it.
const form = useForm({ defaultValues: { email: '' } });

<Form form={form} onSubmit={onSubmit}>
  <Form.Item
    name="email"
    label="Email"
    onChangePropName="onChangeText"
    rules={{ required: 'Email is required' }}
  >
    <TextInput placeholder="[email protected]" />
  </Form.Item>
</Form>;

The package deliberately does not re-export the react-hook-form API. Import anything else you need (resolvers, useWatch, …) from react-hook-form directly.

Features

  • <Form> publishes the useForm() instance you own to its descendants through the official FormProvider plus a package context, so defaultValues, mode and resolver stay under the app's control.
  • <Form.Item> registers its field with useController, clones the single child input and injects the field props — no manual Controller boilerplate.
  • Optional labels and automatic validation messages rendered as React Native Text.
  • Zero runtime dependencies; react, react-native and react-hook-form stay peer dependencies so your app keeps a single copy of each.
  • Ships ESM, CommonJS and type declarations, and is marked "sideEffects": false for tree-shaking.

Installation

npm install react-hook-native-forms

The following packages must be installed by the consuming app (they are peer dependencies and are never bundled):

| Peer dependency | Supported range | | ----------------- | --------------- | | react | ^18 \|\| ^19 | | react-native | >=0.70 | | react-hook-form | ^7.0.0 |

Quick start

import { Pressable, Text, TextInput } from 'react-native';
import { useForm } from 'react-hook-form';
import { Form, useFormHandle } from 'react-hook-native-forms';

type LoginValues = {
  email: string;
  password: string;
};

// React Native has no native form element, so a descendant triggers the submit.
function SubmitButton() {
  const { handleSubmit } = useFormHandle<LoginValues>();

  return (
    <Pressable onPress={() => void handleSubmit()}>
      <Text>Sign in</Text>
    </Pressable>
  );
}

export function LoginScreen() {
  // The screen owns the instance, so its `defaultValues` and `mode` are explicit.
  const form = useForm<LoginValues>({
    defaultValues: { email: '', password: '' },
  });

  return (
    <Form<LoginValues>
      form={form}
      onSubmit={(values) => {
        console.log(values);
      }}
    >
      <Form.Item<LoginValues>
        name="email"
        label="Email"
        onChangePropName="onChangeText"
        rules={{
          required: 'Email is required',
          pattern: {
            value: /\S+@\S+\.\S+/,
            message: 'Enter a valid email address',
          },
        }}
      >
        <TextInput autoCapitalize="none" keyboardType="email-address" />
      </Form.Item>

      <Form.Item<LoginValues>
        name="password"
        label="Password"
        onChangePropName="onChangeText"
        rules={{
          required: 'Password is required',
          minLength: { value: 8, message: 'At least 8 characters' },
        }}
      >
        <TextInput secureTextEntry />
      </Form.Item>

      <SubmitButton />
    </Form>
  );
}

onSubmit is only called once validation passes; otherwise the messages are rendered under the matching fields.

API

<Form>

Publishes the form instance you created with useForm() to every descendant and renders a View wrapping its children.

| Prop | Type | Default | Description | | ---------- | ----------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------- | | form | UseFormReturn<TFieldValues> | — | Required. The useForm() instance owned by the consumer. | | onSubmit | SubmitHandler<TFieldValues> | — | Called with the values once validation succeeds. When omitted, the handleSubmit exposed by useFormHandle does nothing. | | children | ReactNode | — | Required. Form content, typically Form.Item elements. | | style | StyleProp<ViewStyle> | — | Style applied to the wrapping View. |

Form never calls useForm() itself and never forwards useForm options. Configure defaultValues, mode, criteriaMode and resolver where you create the instance:

const form = useForm<LoginValues>({
  defaultValues: { email: '', password: '' },
  mode: 'onBlur',
});

<Form.Item>

Registers a field and binds it to exactly one child element.

| Prop | Type | Default | Description | | ------------------ | -------------------------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------- | | name | FieldPath<TFieldValues> | — | Required. Path of the field in the form values. | | children | ReactElement | — | Required. Exactly one input element. | | rules | UseControllerProps<TFieldValues, TName>['rules'] | — | Validation rules (required, pattern, minLength, validate, …). | | defaultValue | FieldPathValue<TFieldValues, TName> | — | Initial value for this field, forwarded to useController. Form-wide defaults belong in useForm({ defaultValues }). | | label | ReactNode | — | Rendered above the input inside a Text. | | showError | boolean | true | Set to false to suppress the validation message. | | styles | { container?: StyleProp<ViewStyle>; label?: StyleProp<TextStyle>; error?: StyleProp<TextStyle> } | — | Styles for the wrapping View, the label Text and the error Text (red by default). | | onChangePropName | string | 'onChange' | Name of the child prop that receives field.onChange, e.g. onChangeText. | | valuePropName | string | 'value' | Name of the child prop that receives the field value, e.g. checked. | | mapFieldProps | (field, meta) => Record<string, unknown> \| undefined | — | Escape hatch: extra props merged into the child after the injected ones. |

Injected child props

Form.Item spreads the child's own props first, then injects:

| Prop | Source | | ---------- | ---------------- | | name | the name prop | | value | field.value | | onChange | field.onChange | | onBlur | field.onBlur |

The value / onChange prop names are configurable through valuePropName and onChangePropName. React Native's TextInput reports its text through onChangeText and gives onChange a native event without the DOM-style target.value that react-hook-form reads, so bind it with onChangePropName="onChangeText" as the example app does.

Injected props win over props already present on the child. mapFieldProps covers inputs with a different contract again.

useFormHandle<TFieldValues>()

Reads the enclosing Form from context. Returns { control, handleSubmit }, where handleSubmit() already has the Form's onSubmit bound and runs validation first, so a descendant button can simply call it.

The hook is named useFormHandle rather than useFormContext on purpose: react-hook-form already exports a hook called useFormContext, and the two return different things. Import useFormContext from react-hook-form when you need the raw form instance; import useFormHandle from this package when you want the pre-bound submit.

Using Form.Item or useFormHandle outside of a <Form> throws a descriptive error naming the hook.

Other exports

FormItem is exported on its own as well, which is handy when Form.Item would collide with another Item in scope. The public types FormProps, FormItemProps, FormContextValue, FormItemFieldProps, FormItemFieldState and FormSubmitHandler are exported for building typed wrappers around the components.

Notes and limitations

  • name is not inferred from the ancestor Form. React context erases the generic, so Form.Item cannot know the parent's field values type. Pass the generic explicitly (<Form.Item<LoginValues> name="email">) to get autocompletion and a compile-time check of name. A createForm<T>() factory is a possible future enhancement.
  • Child element required. Form.Item needs exactly one element it can clone; passing a string, null or multiple children throws.
  • React Native only. The components render View / Text and inject React Native input props; there is no react-dom build.

Example app

A runnable Expo login form lives in example. It consumes the built package, so build first:

npm run build
cd example
npm install
npm start

The example is not published: the package files field restricts the tarball to dist, and example is excluded from the library tsconfig.json.

Development

Node.js 22.12+ is required for the toolchain (Vite 8, Vitest 5, Changesets 3). CI runs npm run verify on Node 22 and 24.

npm install
npm run verify       # format:check → typecheck → lint → test → build

Run the individual steps when you need them:

npm run format:check  # prettier --check .
npm run typecheck     # tsc --noEmit
npm run lint          # ESLint
npm run test          # Vitest
npm run build         # tsup → dist (ESM + CJS + .d.ts)
npm run format        # prettier --write .

The example app is checked from the root as well; it consumes the built package, so build before running these:

npm run build
npm --prefix example install
npm run typecheck:example  # tsc --noEmit in example/
npm run lint:example       # expo lint in example/

CI runs npm run verify on Node 22 and 24, plus a separate example job that builds the library and then typechecks and lints the example.

The test suite runs against react-native-web in jsdom (see vitest.config.ts), so the React Native components are exercised in a DOM environment.

Breaking change: useFormContext → useFormHandle

The package hook was renamed so it no longer shadows react-hook-form's own useFormContext; import useFormHandle from react-hook-native-forms instead. There is no deprecated alias, and the API now also requires the consumer to own the useForm() instance and pass it as form.

Release workflow

Versioning and the changelog are managed with Changesets:

npx changeset          # describe the change (choose the bump type)
npx changeset version  # bump versions and update CHANGELOG.md
npm run release        # changeset publish

Publishing is still a manual step: the repository has no release workflow yet, and prepublishOnly only re-runs npm run verify before npm publish.

License

MIT