ahmedallem.
Engineering · 4 min read

Getting Started with Allem SDK Forms: Declarative Validation for React

Learn how to use @allem-sdk/forms with useForm, useField, and 9 built-in validators for declarative form management in React.

Ahmed Allem

Ahmed Allem

Founder & CTO · Aviation, AI & Startups

ShareShare
Getting Started with Allem SDK Forms: Declarative Validation for React

@allem-sdk/forms provides declarative form management with built-in validation. Zero dependencies, full TypeScript support, and 9 validators that cover most form scenarios.

Installation

npm install @allem-sdk/forms

Or with the meta-package:

npm install allem-sdk

useForm

The primary hook. Define your fields with initial values and validation rules:

import { useForm, required, email } from "@allem-sdk/forms";

function ContactForm() {
  const form = useForm({
    name: { initialValue: "", rules: [required()] },
    email: { initialValue: "", rules: [required(), email()] },
  });

  const onSubmit = async (values) => {
    await fetch("/api/contact", {
      method: "POST",
      body: JSON.stringify(values),
    });
  };

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <input {...form.getFieldProps("name")} placeholder="Name" />
      {form.touched.name && form.errors.name && (
        <span className="text-red-500">{form.errors.name}</span>
      )}

      <input {...form.getFieldProps("email")} placeholder="Email" />
      {form.touched.email && form.errors.email && (
        <span className="text-red-500">{form.errors.email}</span>
      )}

      <button type="submit" disabled={form.isSubmitting}>
        Send
      </button>
    </form>
  );
}

getFieldProps

The getFieldProps helper returns value, onChange, onBlur, and name — everything a standard input needs:

// This:
<input {...form.getFieldProps("email")} />

// Is equivalent to:
<input
  name="email"
  value={form.values.email}
  onChange={(e) => form.setValue("email", e.target.value)}
  onBlur={() => form.setTouched("email")}
/>

useForm Returns

Property Type Description
values T Current form values
errors Partial<Record<keyof T, string>> Error messages per field
touched Partial<Record<keyof T, boolean>> Which fields have been blurred
isValid boolean True when no errors exist
isSubmitting boolean True during async submit
setValue (field, value) => void Set a field value
setTouched (field) => void Mark a field as touched
validate () => boolean Validate all fields
handleSubmit (onSubmit) => handler Form submit handler
reset () => void Reset to initial values
getFieldProps (field) => props Spread props for an input

Built-in Validators

9 validators cover the most common scenarios:

Validator Usage Description
required() required("Field is required") Must not be empty
minLength(n) minLength(3, "Too short") Minimum string length
maxLength(n) maxLength(100, "Too long") Maximum string length
min(n) min(18, "Must be 18+") Minimum numeric value
max(n) max(100, "Too high") Maximum numeric value
pattern(regex) pattern(/^\d+$/, "Numbers only") Must match regex
email() email("Invalid email") Valid email format
url() url("Invalid URL") Valid URL (http/https)
custom(fn) custom((v) => ...) Custom validation function

All validators accept an optional custom error message as the last argument.

Custom Validators

Use custom() for validation logic that the built-in rules do not cover:

import { useForm, required, custom } from "@allem-sdk/forms";

const passwordStrength = custom<string>((value) => {
  if (value.length < 8) return "Must be at least 8 characters";
  if (!/[A-Z]/.test(value)) return "Must contain an uppercase letter";
  if (!/[0-9]/.test(value)) return "Must contain a number";
  return undefined; // valid
});

const form = useForm({
  password: {
    initialValue: "",
    rules: [required(), passwordStrength],
  },
  confirmPassword: {
    initialValue: "",
    rules: [
      required(),
      custom((value) => {
        if (value !== form.values.password) return "Passwords do not match";
        return undefined;
      }),
    ],
  },
});

useField

A standalone hook for single-field validation. Useful when you need validation on an isolated input:

import { useField, required, email } from "@allem-sdk/forms";

function EmailInput() {
  const emailField = useField({
    initialValue: "",
    rules: [required(), email()],
  });

  return (
    <div>
      <input
        value={emailField.value}
        onChange={(e) => emailField.setValue(e.target.value)}
        onBlur={emailField.onBlur}
        placeholder="you@example.com"
      />
      {emailField.touched && emailField.error && (
        <span className="text-red-500">{emailField.error}</span>
      )}
    </div>
  );
}

useField Returns

Property Type Description
value T Current field value
setValue (value: T) => void Set the value
onBlur () => void Mark as touched
error string | undefined Error message
touched boolean Whether the field has been blurred

Complete Registration Form

import { useForm, required, email, minLength, min, custom } from "@allem-sdk/forms";
import { Button, Input, Select, Card, CardBody, CardHeader } from "@allem-ui/react";

export default function RegistrationForm() {
  const form = useForm({
    name: {
      initialValue: "",
      rules: [required(), minLength(2, "Name must be at least 2 characters")],
    },
    email: {
      initialValue: "",
      rules: [required(), email()],
    },
    age: {
      initialValue: 0,
      rules: [required(), min(18, "Must be at least 18 years old")],
    },
    password: {
      initialValue: "",
      rules: [
        required(),
        minLength(8, "Password must be at least 8 characters"),
        custom((v) => {
          if (!/[A-Z]/.test(v)) return "Must contain an uppercase letter";
          if (!/[0-9]/.test(v)) return "Must contain a number";
          return undefined;
        }),
      ],
    },
  });

  const onSubmit = async (values) => {
    const res = await fetch("/api/register", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(values),
    });
    if (res.ok) alert("Registration successful!");
  };

  return (
    <Card variant="outline" className="max-w-md mx-auto">
      <CardHeader>Create Account</CardHeader>
      <CardBody>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
          <div>
            <Input label="Full Name" {...form.getFieldProps("name")} />
            {form.touched.name && form.errors.name && (
              <p className="text-red-500 text-sm mt-1">{form.errors.name}</p>
            )}
          </div>

          <div>
            <Input label="Email" type="email" {...form.getFieldProps("email")} />
            {form.touched.email && form.errors.email && (
              <p className="text-red-500 text-sm mt-1">{form.errors.email}</p>
            )}
          </div>

          <div>
            <Input label="Age" type="number" {...form.getFieldProps("age")} />
            {form.touched.age && form.errors.age && (
              <p className="text-red-500 text-sm mt-1">{form.errors.age}</p>
            )}
          </div>

          <div>
            <Input label="Password" type="password" {...form.getFieldProps("password")} />
            {form.touched.password && form.errors.password && (
              <p className="text-red-500 text-sm mt-1">{form.errors.password}</p>
            )}
          </div>

          <div className="flex gap-2">
            <Button type="submit" disabled={form.isSubmitting} className="flex-1">
              {form.isSubmitting ? "Creating..." : "Create Account"}
            </Button>
            <Button type="button" variant="outline" onPress={form.reset}>
              Reset
            </Button>
          </div>
        </form>
      </CardBody>
    </Card>
  );
}

What's Next