learn react ui logoLearnReactUI
User Management Form Validation

User Management Form Validation

How to implement effective form validation for a User Management system with fields like username, age, and birthdate. This guide explains how to validate input formats, enforce rules (e.g., age limits, valid usernames), and handle errors gracefully to ensure accurate and user-friendly data collection.

[Demo]

I previously developed a small UserManagement application.

Form Validation1

I will try to develop this application to reach the state shown in the image below. (User Management Validation)

Form Validation2

Let's look at the form structure on the right side of this application today. In the structures I defined:

Form Validation3

The Create Button is not inside the Form Element. It stands in a separate place in the DOM Tree structure.

We expect:

  • Validation to run on entered data when the Create button is pressed
  • If validation is not successful, reflect the result to UI components
  • If validation is successful, send the data obtained from UI to the server

Since the Create button and Form structure are distant from each other in this part, I actually find Formik and Yup validation more suitable.

1. Form Validation Levels

A. Form Level Validation (onSubmit)

Form Submit Validation is the validation process performed on all form data. This part performs the flow shown in the image below.

Form Validation4

  • Client Validation (Some validation operations in the browser)
  • Server Validation (Some validation operations on the server)

After these validation operations, warnings on relevant UI components or more general Toast Messages are shown to the user.

B. Form Element Level Validation (onBlur, onChange)

There is also a need for validation structures that will provide immediate feedback to the user when the field is empty or when the field does not comply with certain limits.

These structures need to trigger the onChange and onBlur events of Form Elements and run the relevant validation again.

2. Technologies

A. With Your Own Method

First, running its own validation structure, catching these change events, calling validation in these operations, storing errors in state, and displaying warnings in relevant parts of screen components.

B. Using Formik and Yup

While we can integrate the validate method we used in our own method with the Formik structure, we can create a Schema with the Yup structure similar to React PropTypes.

const SignInSchema = Yup.object().shape({
  email: Yup.string().email().required("Email is required"),
  password: Yup.string()
    .required("Password is required")
    .min(4, "Password is too short - should be 4 chars minimum"),
});

Then we can pass this Schema to the Formik structure:

<Formik
  initialValues={initialValues}
  validationSchema={signInSchema}
  onSubmit={(values) => {
    console.log(values);
  }}

Although I like the Formik + YUP structure, error integration needs to be done manually in components.

Either we will add the styling structure in components through className natively, or there are stylings provided by UI Component libraries like Antd's Form structures, which can be integrated with Formik + YUP structure.

C. Validation Structures Provided by UI Component Library

In my example, I used the Antd UI Library. Here you can add rules to AntD

elements. And in submit():

<Form<User> form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}>
  <Form.Item name="username" rules={UserRules.username}>
    <Input placeholder="Username" />
  </Form.Item>
  <Form.Item name="age" rules={UserRules.age}>
    <Input placeholder="Age" type="number" />
  </Form.Item>
  <Form.Item name="active" valuePropName="checked">
    <Switch />
  </Form.Item>
  <Form.Item name="email" rules={UserRules.email}>
    <Input placeholder="Email" />
  </Form.Item>
</Form>

After the rules are processed, onFinish and onFinishFailed are executed. This is given according to whether the rules are verified or error status. Rules can be created with a structure similar to Yup:

export const UserRules = {
  username: [{ required: true, message: "Please input your username!" }],
  age: [{ required: true, message: "Please input your age!" }],
  email: [{ required: true, message: "Please input your email!" }],
};

At this point, Antd's internal structure allows for simpler execution of certain validation processes.

Form Validation5

3. Container/Page Component Communication

How will these two structures Create — UserForm Container communicate?

Form Validation6

As you can see in my previous React Architecture articles, I have components communicate with each other through separate stores.

Form Validation7

Since the event in this example takes place entirely on the ClientSide, I thought of setting up a Form State Mechanism on Zustand.

My goals:

  • Create button triggers Form Validation
  • I will use Zustand Store state mechanism for Form Validation triggering
  • I will use Zustand Store again to manage Success/Error state as a result of validation operations
  • I will ensure the progression of operations in this direction.

Form Validation8

4. Technical Implementation

A. Definitions

First, we need to define these 3 parts: Domain Interface, Rule, and Form.

Form Validation9

  • Domain: Object where we store Frontend data. Here User information
  • Rules: Rules to be executed on these data
  • Form: Definition of UI component where we enter user information. Here it's our connection point between Form UI Elements, Domain, and Rule.

B. State Machine (XState)

We set up a State Machine structure on the Zustand store. Let's first visualize this using XState:

Form Validation10

Form Validation State:

import { createMachine, assign } from "xstate";

export interface User {
  id: string;
  username: string;
  age: number;
  active: boolean;
  email: string;
  avatar: string;
  password: string;
  birthdate: Date;
  registeredAt: Date;
}

export enum FormStateEnum {
  Initial = "Initial State",
  InValidation = "In Validation",
  Validated = "Validated",
  Invalid = "Invalid",
}

interface Context {
  formMode: FormStateEnum;
  userInForm: User | null;
}

const validateMachine = createMachine<Context>({
  id: "validate",
  initial: "initial",
  context: {
    formMode: FormStateEnum.Initial,
    userInForm: null,
  },
  states: {
    initial: {
      on: {
        SUBMIT: "validate",
      },
    },
    validate: {
      on: {
        onFinished: "validated",
        onFinishFailed: "invalid",
      },
    },
    validated: {
      type: "final",
    },
    invalid: {
      on: {},
    },
  },
});

C. Client Store (Zustand)

When I prefer a lightweight library instead of Redux, I use Zustand. Here we set up a structure similar to the state machine above within Zustand:

export interface UserInfoFormStoreState {
  formMode: FormStateEnum;
  userInForm: User | null;
  reset: () => void;
  validate: () => void;
  validated: (user: User) => void;
  invalidated: () => void;
}

export const useUserInfoStore = create<UserInfoFormStoreState>(
  (set: SetState<UserInfoFormStoreState>) => ({
    formMode: FormStateEnum.Initial,
    userInForm: null,
    reset: () => {
      set({ formMode: FormStateEnum.Initial, userInForm: null });
    },
    validate: () => {
      set({ formMode: FormStateEnum.InValidation });
    },
    validated: (user: User) => {
      set({ formMode: FormStateEnum.Validated, userInForm: user });
    },
    invalidated: () => {
      set({ formMode: FormStateEnum.Invalid, userInForm: null });
    },
  })
);

Note: The disadvantage of this structure compared to XState is that there is no limitation on which state can transition to another state, whereas in XState, these transition rules are specified. I will provide a more detailed example about XState and Zustand usage later.

D. Flow

  1. First, the Create button is pressed. And create calls Zustand Store validate to bring Form State to "ValidationInProgress".

Form Validation11

  1. Then, the component listening to Mode change in the Form object validates the Form using Antd. At this point, the screen turns red or becomes successful, and we notify the Form State change through zustand again.

Form Validation12

  1. In the final stage, CreateButton components listening to this Form-related State change... If the operation is successful, mutation occurs; otherwise, it can give Toast or warning messages.

Form Validation13

E. useEffect → CustomHook

We had to use useEffect in Form and Create button in this part. Instead, we can make this process more readable by writing a Custom Hook.

Custom Hook writing:

import { useEffect } from "react";
import { FormStateEnum } from "../../types/UserType";

function useFormModeListener(
  store: any,
  callback: (mode: FormStateEnum) => void
) {
  const formMode = store((state: any) => state?.formMode);

  useEffect(() => {
    callback(formMode);
  }, [formMode]);
}

export default useFormModeListener;

This way, we can continue our operations with the Custom Hook method without writing useEffect in every part.

We limit with useFormModeListener. Here we can use the latest state on the store, and we can also use it to change Zustand:

useFormModeListener(useUserInfoStore, (mode: FormStateEnum) => {
  if (mode === FormStateEnum.Validated) {
    mutation.mutate(userInForm);
  } else if (mode === FormStateEnum.Invalid) {
    openNotificationWithIcon("Form Error", "Please check the form again.");
  }
});