Skip to content

useFormState

A powerful React hook for managing form state, validation, and submission — designed to simplify controlled form handling and improve maintainability.


🧩 Overview

useFormState provides a simple yet robust way to manage form values, errors, touched fields, and submission state.
It is a lightweight alternative to form libraries like Formik or React Hook Form, suitable for small to medium forms that need controlled state and validation.


🚀 Usage

tsx
import { useFormState } from "react-hookstack";

function SignupForm() {
  const { values, errors, touched, handleChange, handleBlur, handleSubmit, resetForm } =
    useFormState(
      { name: "", email: "" },
      (values) => {
        const errors: Record<string, string> = {};
        if (!values.name) errors.name = "Name is required";
        if (!values.email.includes("@")) errors.email = "Invalid email";
        return errors;
      }
    );

  return (
    <form onSubmit={handleSubmit(async (vals) => console.log("Submitted:", vals))}>
      <div>
        <label>Name</label>
        <input name="name" value={values.name} onChange={handleChange} onBlur={handleBlur} />
        {touched.name && errors.name && <span>{errors.name}</span>}
      </div>

      <div>
        <label>Email</label>
        <input name="email" value={values.email} onChange={handleChange} onBlur={handleBlur} />
        {touched.email && errors.email && <span>{errors.email}</span>}
      </div>

      <button type="submit">Submit</button>
      <button type="button" onClick={resetForm}>Reset</button>
    </form>
  );
}

⚙️ API Reference

useFormState<T>(initialValues: T, validate?: (values: T) => Partial<Record<keyof T, string>>)

Parameters

NameTypeRequiredDescription
initialValuesTThe initial object of form field values.
validate(values: T) => Partial<Record<keyof T, string>>Optional validation function that returns a map of errors.

Returns

PropertyTypeDescription
valuesTThe current state of all form field values.
errorsPartial<Record<keyof T, string>>The current validation errors for each field.
touchedPartial<Record<keyof T, boolean>>Tracks whether each field has been interacted with.
isSubmittingbooleanIndicates whether the form is currently submitting.
handleChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => voidUpdates a field value when changed.
handleBlur(e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => voidMarks a field as touched and revalidates it.
handleSubmit(onSubmit: (values: T) => void | Promise<void>) => (e: React.FormEvent) => voidWraps your submit handler with built-in validation and submission logic.
resetForm() => voidResets the form to its initial state.

🧠 Example Use Cases

  • Controlled Forms: Keep full control over form fields and validation.
  • Dynamic Field Validation: Run validation logic dynamically on change or blur.
  • Asynchronous Submissions: Perfect for async form handlers (e.g., API calls).
  • Reusable Form Logic: Build reusable form hooks for multiple forms.

💡 Tips

  • The validate function can be synchronous or asynchronous.
  • Combine useFormState with useBoolean to handle form submission states or modals.
  • Supports checkbox inputs — automatically handles boolean checked values.
  • For complex validations, consider schema validators (like Yup) within the validate function.

🧱 Implementation

tsx
import { useState, useCallback } from "react";

export function useFormState<T extends Record<string, any>>(
  initialValues: T,
  validate?: (values: T) => Partial<Record<keyof T, string>>
) {
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
  const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
      const target = e.target;
      const { name, value, type } = target;
      const fieldValue =
        target instanceof HTMLInputElement && type === "checkbox"
          ? target.checked
          : value;

      setValues((prev) => ({ ...prev, [name]: fieldValue }));
      if (validate) setErrors(validate({ ...values, [name]: fieldValue }));
    },
    [validate, values]
  );

  const handleBlur = useCallback(
    (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
      const { name } = e.target;
      setTouched((prev) => ({ ...prev, [name]: true }));
      if (validate) setErrors(validate(values));
    },
    [validate, values]
  );

  const handleSubmit = useCallback(
    (onSubmit: (values: T) => void | Promise<void>) =>
      async (e: React.FormEvent) => {
        e.preventDefault();
        setIsSubmitting(true);
        if (validate) {
          const validationErrors = validate(values);
          setErrors(validationErrors);
          const hasErrors = Object.keys(validationErrors).length > 0;
          if (hasErrors) {
            setIsSubmitting(false);
            return;
          }
        }
        await onSubmit(values);
        setIsSubmitting(false);
      },
    [validate, values]
  );

  const resetForm = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
    setIsSubmitting(false);
  }, [initialValues]);

  return {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit,
    resetForm,
  };
}