Home/Documentation/React Package

React with Recoil

Overview

The Recoil showcase is provided for teams maintaining existing Recoil applications. It shows atoms, selectors, and the same Validation Rules Engine lifecycle as every other React example.

The upstream Recoil repository is archived. Prefer an actively maintained option for new applications, and treat this page as integration and migration support.

Open Live Showcase

Using @validation-rules-engine/react

Use this guide when you already have Recoil in an application. Keep Validation Rules Engine in React components/hooks and keep Recoil atoms focused on plain draft state.

Step 1 — Install Package

Install the React adapter, Core package, and Recoil.

npm install @validation-rules-engine/react @validation-rules-engine/core recoil

Recoil has been archived by its maintainers, so prefer this integration for existing apps rather than new long-lived greenfield architecture.

{
  "dependencies": {
    "recoil": "^0.7.7",
    "@validation-rules-engine/react": "^1.0.0"
  }
}

Step 2 — Configure Project

Wrap the route with RecoilRoot and ValidationRulesProvider.

import { RecoilRoot } from 'recoil';
import { ValidationRulesProvider } from '@validation-rules-engine/react';

export function ProfileRoute() {
  return (
    <RecoilRoot>
      <ValidationRulesProvider>
        <RecoilProfileForm />
      </ValidationRulesProvider>
    </RecoilRoot>
  );
}

Step 3 — Create Validation Policy

Policies target the atom model shape.

import type { ValidationPolicy, ValidationTarget } from '@validation-rules-engine/react';

export type ProfileModel = ValidationTarget & {
  firstName: string;
  lastName: string;
  email: string;
  country: string;
  complianceAccepted: boolean;
};

export const profilePolicy: ValidationPolicy = {
  addValidations: (v) => [
    v.validateFor('firstName').isRequired('First name is required'),
    v.validateFor('lastName').isRequired('Last name is required'),
    v.validateFor('email').isRequired('Email is required').isEmail('Enter a valid email'),
    v.validateFor('country').isRequired('Country is required'),
    v.validateFor('complianceAccepted').isChecked('Compliance acceptance is required')
  ]
};

Step 4 — Register Policy

Register policies through useValidationRules. The hook unregisters them on unmount.

const policies = useMemo(() => [{ name: 'profile', policy: profilePolicy }], []);
const validation = useValidationRules({
  model,
  policies,
  policyNames: ['profile'],
  groups: [{ name: 'profileGroup', policies: ['profile'], formGroups: ['profile'], fields: profileFields }]
});

Step 5 — Connect State Management

Create stable atoms. Allow mutability only when your validation metadata is written onto the model object.

import { atom, selector } from 'recoil';

const initialProfile: ProfileModel = { firstName: '', lastName: '', email: '', country: '', complianceAccepted: false };
export const profileFields = ['firstName', 'lastName', 'email', 'country', 'complianceAccepted'] as const;

export const profileModelState = atom<ProfileModel>({
  key: 'profileModelState',
  default: initialProfile,
  dangerouslyAllowMutability: true
});

export const profileErrorCountState = selector({
  key: 'profileErrorCountState',
  get: ({ get }) => get(profileModelState).validationResults?.length ?? 0
});

Step 6 — Bind Controls

Use useRecoilState for draft values and adapter helpers for validation messages.

const [model, setModel] = useRecoilState(profileModelState);
const emailErrors = validation.getFieldErrors('email');

<input
  value={model.email}
  onChange={(event) => setModel((current) => ({ ...current, email: event.target.value }))}
  onBlur={() => void validation.validateField('email')}
/>
<ValidationMessage errors={emailErrors} />
<ValidationSummary errors={validation.errors} />

Step 7 — Validate

Validate the atom value and store the decorated clone.

async function submit() {
  const snapshot = await validation.validate({ showAllErrors: true });
  setModel(structuredClone(validation.model));
  if (snapshot.isValid) await saveProfile(validation.model);
}

await validation.validateField('email');
await validation.validateGroup('profileGroup');

Step 8 — Reset

Clear adapter state and replace the atom value.

function reset() {
  validation.clear();
  setModel(initialProfile);
}

Recoil atom cleanup is controlled by RecoilRoot; adapter policy cleanup is controlled by the hook.

useEffect(() => () => setModel(initialProfile), [setModel]);

Step 9 — Best Practices

Use selectors for derived display values instead of duplicating validation status.

const invalidFieldsState = selector({
  key: 'profileInvalidFieldsState',
  get: ({ get }) => (get(profileModelState).validationResults ?? []).map((result) => result.propertyName)
});

Plan a migration path for long-lived products, keep atom keys stable, and isolate any React compatibility shims outside validation code.

Step 10 — Complete Working Example

import { FormEvent, useMemo } from 'react';
import { useRecoilState } from 'recoil';
import { ValidationMessage, ValidationSummary, useValidationRules } from '@validation-rules-engine/react';
import { ProfileModel, profilePolicy } from './profilePolicy';
import { profileFields, profileModelState } from './profileRecoilState';

export function RecoilProfileForm() {
  const [model, setModel] = useRecoilState(profileModelState);
  const policies = useMemo(() => [{ name: 'profile', policy: profilePolicy }], []);
  const groups = useMemo(() => [{ name: 'profileGroup', policies: ['profile'], formGroups: ['profile'], fields: [...profileFields] }], []);
  const validation = useValidationRules<ProfileModel>({ model, policies, policyNames: ['profile'], groups });
  const emailErrors = validation.getFieldErrors('email');

  async function submit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const snapshot = await validation.validate({ showAllErrors: true });
    setModel(structuredClone(validation.model));
    if (snapshot.isValid) await saveProfile(validation.model);
  }

  return (
    <form onSubmit={submit} noValidate>
      <ValidationSummary errors={validation.errors} />
      <input aria-label="Email" value={model.email} onChange={(event) => setModel((current) => ({ ...current, email: event.target.value }))} onBlur={() => void validation.validateField('email')} />
      <ValidationMessage errors={emailErrors} />
      <label>
        <input type="checkbox" checked={model.complianceAccepted} onChange={(event) => setModel((current) => ({ ...current, complianceAccepted: event.target.checked }))} />
        Compliance accepted
      </label>
      <button type="submit">Save</button>
      <button type="button" onClick={() => { validation.clear(); setModel({ firstName: '', lastName: '', email: '', country: '', complianceAccepted: false }); }}>Reset</button>
    </form>
  );
}

Installation

npm install @validation-rules-engine/react recoil

Package imports

import { atom, selector, RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
import { useValidationRules, useValidationField, ValidationSummary } from '@validation-rules-engine/react';

Provider setup

Wrap the route with RecoilRoot and initialize the model atom.

<RecoilRoot initializeState={({ set }) => set(profileAtom, initialModel)}>
  <ProfileForm />
</RecoilRoot>

Policy registration

Read the atom value and register policies with the React adapter.

const model = useRecoilValue(profileAtom);
const form = useValidationRules({ model, policies, policyNames: ['profile'], groups });

Policy unregistration

The hook unregisters policies on unmount. Reset atoms when leaving long-lived Recoil roots.

Validation lifecycle

Field updates write to atoms. Validation helpers evaluate the current atom snapshot and publish metadata through the bridge.

Validation Groups

Use selectors for group status summaries when existing Recoil screens already use derived state.

Validation Summary

<ValidationSummary errors={form.errors} />

Custom Inputs

Custom inputs should write through useRecoilState or a setter hook and use validation metadata for ARIA and message rendering.

Performance Considerations

Keep the form route boundary small. Recoil is best shown here for existing codebases; new projects should weigh maintenance status before choosing it.

Troubleshooting

If React compatibility issues appear, isolate Recoil usage behind the route provider and keep validation policies independent of Recoil APIs.

Complete code example

const profileAtom = atom({ key: 'profile', default: { firstName: '', email: '' } });
const populatedCount = selector({
  key: 'profilePopulatedCount',
  get: ({ get }) => Object.values(get(profileAtom)).filter(Boolean).length
});

function ProfileForm() {
  const [model, setModel] = useRecoilState(profileAtom);
  const form = useExternalValidationBridge({
    model,
    setFieldValue: (path, value) => setModel((current) => ({ ...current, [path]: value })),
    policies,
    policyNames: ['profile']
  });

  return <ProfileFields form={form} populated={useRecoilValue(populatedCount)} />;
}

Architecture

A RecoilRoot initializes a model atom and revision atom. A selector derives populated-value state. Because Recoil 0.7 checks a React 18 internal dispatcher name, the React 19 showcase includes a narrow app-only compatibility alias; no adapter or engine API is changed.

Controls → Recoil atoms → selector → validation hooks → core policies

Why use this state management library

Choose this integration when an existing Recoil application needs Validation Rules Engine. For greenfield work, account for the project's archived status before adopting it.

How Validation Rules Engine integrates

Atom setters own model transitions. Recoil selectors provide derived UI state, while validation policies stay outside the state graph.

Best Practices

  • Keep RecoilRoot ownership explicit.
  • Use selectors for derived values rather than duplicated atoms.
  • Isolate the React 19 compatibility layer to the application.
  • Plan a migration path for long-lived products.

Common Mistakes

  • Ignoring Recoil's archived maintenance status.
  • Freezing values that the current validation engine annotates during evaluation.
  • Creating atom keys dynamically without stable uniqueness.
  • Storing engine instances inside atoms.

Code Example

const modelState = atom({
  key: 'vreShowcaseModel',
  default: initialModel,
  dangerouslyAllowMutability: true
});
const populatedState = selector({
  key: 'vreShowcasePopulated',
  get: ({ get }) => countPopulatedValues(get(modelState))
});

Open Live Showcase

Continue in the live platform

Open React ShowcaseTry the hooks and policies in a live React application →