# RVF keeps validation on the server and the form on the platform

> RVF, formerly Remix Validated Form, is a form library for React that leans on native form APIs rather than replacing them, so a form built with it keeps working when the JavaScript never arrives. The design is that the same validator runs on the server and in the browser, and that form state is addressed through scopes rather than through a pile of component state.

**airjp73/rvf** — Easy form validation and state management for React and Remix

- Repository: https://github.com/airjp73/rvf
- Website: https://rvf-js.io
- Stars: 967 · Forks: 77
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/airjp73-rvf

## A form library that tries to get out of the way

The positioning in the README is short. RVF is easy form validation and state management for React, and it used to be called Remix Validated Form, which is a good clue about where it came from and who it is for. The first feature is described as progressively enhanced, because RVF leans into native form APIs so it is easy to add to existing forms, and it even works without JavaScript on the client if you are using a server rendered framework like Remix or React Router, or Next.js. That is the claim that matters, because most React form libraries are the opposite, they are JavaScript that renders a form and would show nothing at all without it. The second feature is about scale. When you need to write bigger and more complicated forms, RVF scales with you, and the three specific things it promises are nested objects and arrays managed in a type safe way, even when they are recursive, default values for the whole form set in one place, and the ability to reuse your validation on the server.

## Scopes are how you address a value inside a nested form

The API is easier to understand once you see that the form state is addressed by path rather than by prop drilling. In the plain React example, the form object is created with `useForm` and given a validator, a set of default values, a `handleSubmit` and an `onSubmitSuccess` callback. A field then binds to a scope, `form.scope("projectName")` for a top level value and `item.scope("title")` for a value inside an array item, and the bound component receives that scope as a prop. Errors are read the same way, with `form.error("tasks")` for the array level message. Arrays get their own namespace, since `form.array("tasks")` is a collection you can map over, push to, remove from and measure with `length()`. The example pushes a new item and then focuses the field that was just created, using `form.focus` with a path like `tasks[0].title`, which is a small detail that tells you the library keeps track of the shape of the form rather than just the values.

## One validator, run in the browser and on the server

The React Router example is the one to study, because it shows the whole point of the library. The validator is a Zod schema wrapped with `withZod`, and it is used in two places. On the client, the same object is handed to `useForm` as the validator, so the browser can check the values as the user types. On the server, the action reads the posted form data and validates it with the same schema:

```tsx
export const action = async ({ request }: ActionFunctionArgs) => {
  const data = await validator.validate(await request.formData());
  if (data.error) return validationError(data.error);

  const { projectName, tasks } = data.data;

  await createProject({ name: projectName, tasks });
  return json({ projectName });
};
```

The interesting parts are the error path and the type narrowing. A failed validation returns a validation error response rather than throwing, and the client checks it with `isValidationErrorResponse`, which is a type guard, so the success callback can read `data.projectName` without a cast, and the comment in the example says exactly that, that TypeScript does not know this is not an error in the success callback. So the library is not only validating twice, it is preserving the type information across the server boundary, which is the part that usually gets lost.

## Four packages, and the one you pick depends on your stack

The examples show the package split, and it is worth mapping it out because the import path tells you which integration you are using. `@rvf/react` provides `useForm` for the plain React case, `@rvf/react-router` provides the Remix and React Router bindings, including `isValidationErrorResponse` and `validationError`, and `@rvf/zod` provides the `withZod` adapter that turns a Zod schema into something the core understands. Underneath is `@rvf/core`, which the root manifest depends on directly with a wildcard version, and which is where the framework agnostic parts live, the validator interface, the scope model and the form state. The practical consequence is that the Zod dependency is optional in principle, since the core is defined in terms of a validator interface, so a project that already has a validation library can adapt it rather than adopt Zod.

## The monorepo runs on bun and turbo

The repository is a bun workspace, and the root manifest still carries its old name, `remix-validated-form-monorepo`, which is a small reminder of the project's history. The workspaces are `packages/*`, `apps/test-app` and `apps/docs`, and the scripts are organised around turbo, with `build`, `dev`, `test` and `typecheck` all fanning out across the workspace, and a separate `build-docs` that forces a rebuild of the docs. The apps are run with bun rather than through turbo, with `bun dev --filter=sample-app` and a `test-app` variant, and the test runner is vitest, with a `vitest:run` script for a single pass, and there is a `vitest.setup.ts` at the root. Linting and formatting are eslint with a cache flag and prettier with a cache flag, husky installs the commit hooks through the prepare script, and there is a `publish-rvf.sh` script in the root for cutting the packages. The engine requirements are Node 20 or newer and npm 7 or newer, with bun 1.3.0 declared as the package manager.

## The version history says what the project considers safe

The release list is short and it tells you the project's policy. v8.0.0 in August 2025 is described as small, rarely-breaking changes, v8.1.0 in September 2025 as small improvements, and the most recent release, react-router-8.2.0 in August 2026, is explicitly for React Router 8 support. That last one is the useful piece of information for an adopter, because it says the React Router integration has been versioned alongside the framework, which is what you want from a library that returns typed responses from your actions. The naming of the release rather than just the library version also shows the project treats the framework binding as a separate shippable unit. The repository carries a `TODO.md` at the root next to the readme and a licence file, and the last push was on 2026-10-15, so the work is current even though the version numbers have not moved since the React Router 8 release.

## Conclusion

RVF suits a team that wants one validation definition to rule both the browser and the server action, especially one on Remix or React Router where the form data arrives as a form post. It is a poor fit if your forms are a small part of a client only application, since the framework agnostic core has more concepts than a simple controlled component needs. It is also a poor fit if you are on an old React Router, since the React Router 8 support arrived as a separate release in August 2026. Before you adopt it, check three things: which package you want, since the core, the React bindings, the React Router bindings and the Zod adapter are separate, how your server action returns errors, since the React Router example returns a validation error response that the client checks with a type guard, and the version you pin, with v8.0.0 in August 2025, v8.1.0 in September and the React Router 8 release in August 2026.

## FAQ

### What is RVF and what did it used to be called?

RVF is easy form validation and state management for React, formerly called Remix Validated Form. It leans on native form APIs and can work without JavaScript on the client when the app is server rendered with Remix, React Router or Next.js.

### How do I read and write nested values in an RVF form?

Through scopes. A field binds to `form.scope("projectName")` for a top level value or `item.scope("title")` for a value inside an array item, errors are read with `form.error("tasks")`, and arrays are handled through `form.array("tasks")`, which you can map over, push to and remove from, and the library keeps the shape of recursive and nested values in a type safe way.

### Can RVF reuse the same validation on the server?

Yes, and that is a headline feature. The React Router example validates the posted form data in the action with the same wrapped Zod validator the client uses, returns a validation error response if it fails, and the client narrows the result with a type guard before reading the success data.

### Which packages does RVF publish?

At least four, with `@rvf/react` for plain React, `@rvf/react-router` for the Remix and React Router bindings including the validation error helpers, `@rvf/zod` for the Zod adapter, and `@rvf/core` underneath, which the root workspace manifest depends on directly.

### Does RVF work with React Router 8?

Yes, and it is the most recent release, react-router-8.2.0, published on 2026-08-07 with the note that it is for React Router 8 support. Before that came v8.1.0 in September 2025 and v8.0.0 in August 2025, described as small, rarely-breaking changes.

## Sources

- [airjp73/rvf on GitHub](https://github.com/airjp73/rvf)
- [License: MIT](https://github.com/airjp73/rvf/blob/main/LICENSE)
- [Project website](https://rvf-js.io)
- [README](https://github.com/airjp73/rvf/blob/main/README.md)
- [Releases](https://github.com/airjp73/rvf/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/airjp73-rvf
