Home/Documentation/React Package

React with Redux Toolkit

Overview

The Redux Toolkit showcase proves that validation policies can remain framework-neutral while a Redux slice owns form transitions and memoized selectors expose the active model and derived state.

Open Live Showcase

Using @validation-rules-engine/react

Redux Toolkit should own form transitions and serializable draft state. @validation-rules-engine/react should stay in React components/hooks, where it can register policies and validate the selected draft.

Step 1 — Install Package

Install the React adapter, Core package, Redux Toolkit, and React Redux.

npm install @validation-rules-engine/react @validation-rules-engine/core @reduxjs/toolkit react-redux

Keep React Redux and Redux Toolkit versions aligned with your React version.

{
  "dependencies": {
    "@reduxjs/toolkit": "^2.0.0",
    "react-redux": "^9.0.0",
    "@validation-rules-engine/react": "^1.0.0"
  }
}

Step 2 — Configure Project

Wrap the feature with both Redux and Validation Rules Engine providers.

import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import { ValidationRulesProvider } from '@validation-rules-engine/react';
import { profileReducer } from './profileSlice';

const store = configureStore({ reducer: { profile: profileReducer } });

export function ProfileRoute() {
  return (
    <Provider store={store}>
      <ValidationRulesProvider>
        <ReduxProfileForm />
      </ValidationRulesProvider>
    </Provider>
  );
}

Step 3 — Create Validation Policy

Policies validate the selected Redux draft.

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

export type ProfileDraft = ValidationTarget & {
  firstName: string;
  lastName: string;
  email: string;
  accepted: 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('accepted').isChecked('Acceptance is required')
  ]
};

Step 4 — Register Policy

Use useValidationRules to register policies and groups for the selected Redux model.

const policies = useMemo(() => [{ name: 'profile', policy: profilePolicy }], []);
const groups = useMemo(() => [
  { name: 'profileGroup', policies: ['profile'], formGroups: ['profile'], fields: ['firstName', 'lastName', 'email', 'accepted'] }
], []);

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

Step 5 — Connect State Management

Reducers store values only. Validation metadata is committed only after the component validates a draft.

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { ProfileDraft } from './profilePolicy';

const initialState: ProfileDraft = { firstName: '', lastName: '', email: '', accepted: false };

const profileSlice = createSlice({
  name: 'profile',
  initialState,
  reducers: {
    fieldChanged: (state, action: PayloadAction<{ path: keyof ProfileDraft; value: unknown }>) => {
      state[action.payload.path] = action.payload.value as never;
    },
    validated: (_state, action: PayloadAction<ProfileDraft>) => action.payload,
    reset: () => initialState
  }
});

export const profileActions = profileSlice.actions;
export const profileReducer = profileSlice.reducer;
export const selectProfile = (state: { profile: ProfileDraft }) => state.profile;

Step 6 — Bind Controls

Bind selected state to inputs and dispatch changes. Use adapter helpers for messages and summaries.

const emailErrors = validation.getFieldErrors('email');

<input
  aria-describedby="email-errors"
  aria-invalid={emailErrors.length > 0}
  value={model.email}
  onChange={(event) => dispatch(profileActions.fieldChanged({ path: 'email', value: event.target.value }))}
  onBlur={() => void validation.validateField('email')}
/>
<ValidationMessage id="email-errors" errors={emailErrors} />
<ValidationSummary errors={validation.errors} />

Step 7 — Validate

Validate in the component and dispatch a validated snapshot if the model should keep validation metadata.

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

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

Step 8 — Reset

Clear validation state and dispatch the slice reset action.

function reset() {
  validation.clear();
  dispatch(profileActions.reset());
}

Policy unregistration is automatic when the hook unmounts.

useEffect(() => () => dispatch(profileActions.reset()), [dispatch]);

Step 9 — Best Practices

Keep reducers pure and keep the validation engine out of Redux state.

type SerializableProfileState = Pick<ProfileDraft, 'firstName' | 'lastName' | 'email' | 'accepted'>;

Use selectors for derived UI state, dispatch semantic actions, and validate in thunks/effects only when the validation operation is part of an async workflow.

Step 10 — Complete Working Example

import { FormEvent, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { ValidationMessage, ValidationSummary, useValidationRules } from '@validation-rules-engine/react';
import { ProfileDraft, profilePolicy } from './profilePolicy';
import { profileActions, selectProfile } from './profileSlice';

export function ReduxProfileForm() {
  const dispatch = useDispatch();
  const model = useSelector(selectProfile);
  const policies = useMemo(() => [{ name: 'profile', policy: profilePolicy }], []);
  const groups = useMemo(() => [{ name: 'profileGroup', policies: ['profile'], formGroups: ['profile'], fields: ['firstName', 'lastName', 'email', 'accepted'] }], []);
  const validation = useValidationRules<ProfileDraft>({ 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 });
    dispatch(profileActions.validated(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) => dispatch(profileActions.fieldChanged({ path: 'email', value: event.target.value }))}
        onBlur={() => void validation.validateField('email')}
      />
      <ValidationMessage errors={emailErrors} />
      <label>
        <input type="checkbox" checked={model.accepted} onChange={(event) => dispatch(profileActions.fieldChanged({ path: 'accepted', value: event.target.checked }))} />
        Accept terms
      </label>
      <button type="submit">Save</button>
      <button type="button" onClick={() => { validation.clear(); dispatch(profileActions.reset()); }}>Reset</button>
    </form>
  );
}

Installation

npm install @validation-rules-engine/react @reduxjs/toolkit react-redux

Package imports

import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useDispatch, useSelector } from 'react-redux';
import { useValidationRules, useValidationField, ValidationSummary } from '@validation-rules-engine/react';

Provider setup

Create a scoped store for the form route and render it under both Redux and Validation Rules Engine providers.

<Provider store={store}>
  <ValidationRulesProvider>
    <ProfileForm />
  </ValidationRulesProvider>
</Provider>

Policy registration

Read the Redux model with a selector and pass it to useValidationRules.

const model = useSelector(selectProfileDraft);
const validation = useValidationRules({ model, policies, policyNames: ['profile'], groups });

Policy unregistration

The React hook unregisters policies on unmount. Redux cleanup should reset the draft slice when leaving the route if the draft should not persist.

Validation lifecycle

Dispatch field updates, then call validation.validateField(path) for focused validation or validation.validate({ showAllErrors: true }) for submit.

Validation Groups

Store group status on the validated model or derive section badges from validation.errors and the group field list.

Validation Summary

<ValidationSummary errors={validation.errors} />

Custom Inputs

Use useValidationField for metadata and dispatch the Redux action from onChange.

const field = useValidationField(formBridge, 'email', { validateOnChange: true });
dispatch(profileSlice.actions.fieldChanged({ path: 'email', value }));

Performance Considerations

Keep selectors focused, store serializable form values, and avoid placing mutable validation metadata in long-lived global state unless the route owns cleanup.

Troubleshooting

If Redux Toolkit freezes state, clone or serialize the form model before validation metadata is written. Keep policies stable so registration does not churn on every dispatch.

Complete code example

const slice = createSlice({
  name: 'profile',
  initialState: { firstName: '', email: '' },
  reducers: {
    fieldChanged(state, action) {
      state[action.payload.path] = action.payload.value;
    },
    reset: () => ({ firstName: '', email: '' })
  }
});

function ProfileForm() {
  const dispatch = useDispatch();
  const model = useSelector((state) => state.profile);
  const form = useReduxValidationBridge({
    model,
    setFieldValue: (path, value) => dispatch(slice.actions.fieldChanged({ path, value })),
    policies: [{ name: 'profile', policy: profilePolicy }],
    policyNames: ['profile']
  });

  return (
    <form onSubmit={form.handleSubmit(async () => save(model))}>
      <ValidationSummary errors={form.errors} />
      <ConnectedInput form={form} path="email" label="Email" />
      <button type="submit">Save</button>
    </form>
  );
}

Architecture

configureStore hosts a dedicated slice. Slice actions replace or reset the serializable model, and selectors read the model, revision, and populated-value count. The React adapter receives the selected model through the shared showcase bridge.

Controls → slice actions → Redux store → selectors → validation hooks → core

Why use this state management library

Choose Redux Toolkit when form state participates in a larger application workflow, explicit events matter, Redux DevTools are valuable, or several distant features consume the same draft.

How Validation Rules Engine integrates

Validation remains outside reducers. Reducers store form transitions; hooks run policies against the selected model and dispatch the next immutable model after field changes.

Best Practices

  • Keep reducers pure and state serializable.
  • Use memoized selectors for model decoding and derived data.
  • Scope transient forms unless global persistence is intentional.
  • Dispatch semantic reset and replace actions.

Common Mistakes

  • Running validation side effects inside reducers.
  • Selecting the entire application state for every input.
  • Storing non-serializable engine instances in Redux.
  • Confusing server entities with editable drafts.

Code Example

const slice = createSlice({
  name: 'validationShowcase',
  initialState,
  reducers: {
    modelReplaced(state, action) { state.serializedModel = JSON.stringify(action.payload); },
    modelReset(state, action) { state.serializedModel = JSON.stringify(action.payload); }
  }
});

const model = useSelector(selectModel);
dispatch(slice.actions.modelReplaced(nextModel));

Open Live Showcase

Continue in the live platform

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