per-form React

website·Indexed 29 days ago

https://tonai.github.io/react-per-form

A fast and easy form validation library for React based on native form validation. It provides a set of hooks and components including useForm, useWatch, useInput, and <Form> to manage form state and validation with a focus on performance by utilizing uncontrolled components.

Tokens
16.3K
Snippets
72
Records
85
Agent score
70%

What's inside per-form

  1. Overview of @per-form/react form validation

    @per-form/react is a fast, lightweight form validation library for React that leverages native HTML capabilities. It supports both controlled components and external UI libraries, and is compatible with Next.js server actions. Key features include the ability to work without manual state management, support for async and cross-input validation, dynamic forms, and customizable error messages/translations.
  2. Initialize a form using the <Form> component

    The <Form> component provides an alternative way to initialize a @per-form/react form. It internally handles the setup by using useForm and <FormProvider>, allowing you to wrap your form fields without manually managing the form context provider.
  3. Implement local field validation with useInput

    When splitting a complex form into multiple components, you can declare validators at the field level using the useInput hook. This allows the validation logic to reside within the component that renders the input. By default, local errors are not present in the form-level errors object for performance reasons; to enable this, set filterLocalErrors: false in the useForm or <Form> configuration.
    import { Form, useInput, type IFormValues } from '@per-form/react';
    
    const validator = (values: IFormValues) =>
      String(values.text).includes('foo') ? '' : 'Value does not include "foo"';
    
    function Input() {
      // Pass the field name and validator to useInput
      const { errors } = useInput({ name: 'text', validator });
      return (
        <>
          <input name="text" required />
          {errors.all.text && <div className="error">{errors.all.text}</div>}
        </>
      );
    }
    
    export default function Demo() {
      return (
        <Form onSubmit={(e, values) => console.log(values)}>
          <Input />
          <button type="submit">Submit</button>
        </Form>
      );
    }
  4. Implement a submit button using useFormStates

    This example demonstrates how to use useFormStates within a child component to disable a submit button during the submission process and display the current form state as JSON.
    import { FormProvider, useForm, useFormStates } from '@per-form/react';
    
    function Submit() {
      const states = useFormStates();
      return (
        <>
          <div className="actions">
            <button disabled={states.isSubmitting} type="submit">
              Submit
            </button>
            <button type="reset">Reset</button>
          </div>
          <pre>{JSON.stringify(states, null, 2)}</pre>
        </>
      );
    }
    
    export default function Demo() {
      const { formProps, ...context } = useForm({
        defaultValues: { a: 'foo' },
        onSubmit: async (e, values) => { console.log(values); },
      });
    
      return (
        <FormProvider {...context}>
          <form {...formProps}>
            <input name="a" required />
            <Submit />
          </form>
        </FormProvider>
      );
    }
  5. Implement custom field validation

    To add validation to a specific field, use the validators parameter in useForm or the <Form> component. The validators object should have keys matching the field names. Each validator function receives the current form values and should return an error string if validation fails, or an empty string if it passes. To optimize performance, declare the validators object outside the component to avoid creating a new reference on every render.

    When using this simple syntax, the error is accessible via errors.all[fieldName].

    import { useForm } from '@per-form/react';
    
    const validators = {
      text: (values) =>
        String(values.text).includes('foo') ? '' : 'Value does not include "foo"',
    };
    
    export default function Demo() {
      const { errors, formProps } = useForm({
        onSubmit: (e, values) => console.log(values),
        validators,
      });
    
      return (
        <form {...formProps}>
          <input name="text" required />
          {errors.all.text && <div className="error">{errors.all.text}</div>}
          <button type="submit">Submit</button>
        </form>
      );
    }
  6. Define local custom messages for specific inputs

    For granular control, you can provide local custom messages using the useInput or useInputs hooks. Local messages defined within useInput will override or supplement the global messages provided to the form.
    const localMessages = { fooError: 'Value does not include "foo"' };
    
    function Input() {
      const { errors } = useInput({
        name: 'text',
        messages: localMessages,
        validator: (values) => String(values.text).includes('foo') ? '' : 'fooError',
      });
    
      return (
        <>
          <input name="text" required />
          {errors.all.text && <div>{errors.all.text}</div>}
        </>
      );
    }
  7. Implement a simple controlled component in @per-form/react

    @per-form/react primarily uses uncontrolled components for performance, but controlled components are useful for real-time reactions (e.g., filtering, searching) or when using UI libraries designed for controlled state. Since @per-form/react does not manage your local state, you must handle state updates and form resets manually. Use the onReset parameter in useForm or the <Form> component to synchronize your local state when the form is reset.
    import { type ChangeEvent, type FormEvent, useState } from 'react';
    import { type IFormValues, useForm } from '@per-form/react';
    
    export default function Demo() {
      const [value, setValue] = useState('');
    
      function handleChange(event: ChangeEvent<HTMLInputElement>) {
        setValue(event.target.value);
      }
    
      function handleReset() {
        setValue('');
      }
    
      const { errors, formProps } = useForm({
        onReset: handleReset,
        onSubmit: (e, values) => console.log(values),
      });
    
      return (
        <form {...formProps}>
          <input name="text" onChange={handleChange} value={value} />
          {errors.all.text && <div>{errors.all.text}</div>}
          <button type="reset">Reset</button>
        </form>
      );
    }
  8. Implement asynchronous validation

    Validators in @per-form/react can be asynchronous. If a validator function returns a Promise that resolves to an error string (or an empty string for success), the library will handle the async state accordingly.
    const validators = {
      text: (values) =>
        // Assume delay returns a Promise
        delay(String(values.text).includes('foo') ? '' : 'Value does not include "foo"'),
    };
  9. Subscribe to field changes with the watch function

    The watch function can be retrieved from the useForm hook or useFormContext. It allows you to execute a callback whenever specified field values change.

    It accepts two parameters:

    1. Callback: A function fired when values change.
    2. Fields: A string, array of strings, or nothing (to watch all values).

    The watch function returns an unsubscribe function, which should be returned within a useEffect cleanup to prevent memory leaks.

    import { useEffect, useState } from 'react';
    import { useForm } from '@per-form/react';
    
    export default function Demo() {
      const { formProps, watch } = useForm({
        onSubmit: (e, values) => console.log(values),
      });
    
      const [text, setText] = useState('');
    
      useEffect(() => {
        // Subscribe to 'text' field and update local state
        // The return value of watch is the unsubscribe function
        return watch<{ text: string }>(({ text }) => setText(text), 'text');
      }, [watch]);
    
      return (
        <form {...formProps}>
          <input name="text" required />
          <div>value = {text}</div>
        </form>
      );
    }
  10. Style form fields based on validation state

    There are three primary ways to style validation states in @per-form/react:

    1. CSS Pseudo-classes: Use :valid and :invalid. These apply immediately based on the internal validation state, regardless of whether useNativeValidation is enabled.
    2. CSS Classes: Use the errors object to apply a class (e.g., .has-error). This typically applies only after a form submission attempt.
    3. CSS :has selector: Use a selector like input:has(+ .error) to style an input if it is immediately followed by an error element.
    /* Pseudo-class approach */
    input:invalid {
      border: 1px solid red;
    }
    
    /* Class approach */
    .has-error {
      background-color: rgba(255, 0, 0, 0.1);
      border: 1px solid red;
    }
    
    /* Combined approach: only show green if it becomes valid AFTER having an error */
    .has-error:valid {
      background-color: rgba(0, 255, 0, 0.1);
      border: 1px solid green;
    }
  11. Implement local cross-field validation with useInputs

    To validate multiple fields together within a local component, use the useInputs hook. You provide an array of field names and a validator function. The system automatically generates an ID for the validator based on the comma-separated names (e.g., 'a,b'), but you can override this by providing a custom id or passing an object to the validators property.
    import { Form, useInputs, type IFormValues } from '@per-form/react';
    
    const validator = (values: IFormValues) =>
      Number(values.a) + Number(values.b) === 42
        ? ''
        : 'The sum must be equal to 42';
    
    function Inputs() {
      const names = ['a', 'b'];
      // Use useInputs for cross-field validation
      const { errors } = useInputs({ names, validators: validator });
      
      return (
        <>
          <input name="a" required type="number" />
          <input name="b" required type="number" />
          {/* Access error via the generated comma-separated name key */}
          {errors.validator['a,b']?.error && (
            <div className="error">{errors.validator['a,b'].error}</div>
          )}
        </>
      );
    }
    
    export default function Demo() {
      return (
        <Form onSubmit={(e, values) => console.log(values)}>
          <Inputs />
          <button type="submit">Submit</button>
        </Form>
      );
    }
  12. Implement dynamic field validation

    For forms with a dynamic number of fields, you can update the names array passed to a cross-field validator. By passing a state-driven array to the names property of the validator configuration, the validation will automatically apply to the current set of dynamic fields.
    import { useState } from 'react';
    import { useForm } from '@per-form/react';
    
    function validator(values) {
      return Object.values(values).reduce((a, b) => Number(a) + Number(b), 0) === 42
        ? ''
        : 'The sum must be equal to 42';
    }
    
    export default function Demo() {
      const [names, setNames] = useState<string[]>([]);
    
      const { errors, formProps } = useForm({
        onSubmit: (e, values) => console.log(values),
        validators: { 
          sum: { names, validator } 
        },
      });
    
      return (
        <form {...formProps}>
          {names.map((name) => (
            <input key={name} name={name} type="number" />
          ))}
          {errors.validator.sum?.error && <div>{errors.validator.sum.error}</div>}
        </form>
      );
    }