0.1.2 • Published 7 months ago

reacty-form v0.1.2

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

Reacty-form

Reacty-form is a React form management library that provides a set of hooks and components (very similar to react-hook-form) to manage form state efficiently. It leverages the power of Legend-State for state management, offering fine-grained reactivity and performance optimizations.

Installation

npm install reacty-form
# or
yarn add reacty-form
# or
pnpm add reacty-form

Getting Started

Here's a basic example of using Reacty-form in a React application:

import React from 'react';
import { Controller, useForm } from 'reacty-form';

function App() {
    const { register, formState: { errors } } = useForm();

    const onSubmit = (data) => {
        console.log(data);
    };

    return (
        <form onSubmit={form.handleSubmit(onSubmit)}>
            <input {...register('name')} />
            {errors.name && <p>Name is required.</p>}
            <input type="submit" />
        </form>
    );
}

export default App;

API Reference

useForm

useForm is a custom hook for managing forms with ease. It takes one object as an optional argument. The following example demonstrates all of its properties along with their default values.

Generic props:

modeValidation strategy before submitting behaviour.
reValidateModeValidation strategy after submitting behaviour.
defaultValuesDefault values for the form.
valuesReactive values to update the form values.
resetOptionsOption to reset form state update while updating new form values.
criteriaModeDisplay all validation errors or one at a time.
shouldFocusErrorEnable or disable built-in focus management.
delayErrorDelay error from appearing instantly.

Schema validation props:

resolverIntegrates with your preferred schema validation library. (it accepts the resolver from @hookform/resolver)
contextA context object to supply for your schema validation.

Props:

  • mode: onChange | onBlur | onSubmit | onTouched | all = 'onSubmit'

    This option allows you to configure the validation strategy before a user submits the form. The validation occurs during the onSubmit event, which is triggered by invoking the handleSubmit function.

    NameTypeDescription
    onSubmitstringValidation is triggered on the submit event, and inputs attach onChange event listeners to re-validate themselves.
    onBlurstringValidation is triggered on the blur event.
    onChangestringValidation is triggered on the changeevent for each input, leading to multiple re-renders. Warning: this often comes with a significant impact on performance.
    onTouchedstringValidation is initially triggered on the first blur event. After that, it is triggered on every change event. Note: when using with Controller, make sure to wire up onBlur with the render prop.
    allstringValidation is triggered on both blur and change events.
  • reValidateMode: onChange | onBlur | onSubmit = 'onChange'

    This option allows you to configure validation strategy when inputs with errors get re-validated after a user submits the form (onSubmit event and handleSubmit function executed). By default, re-validation occurs during the input change event.

  • defaultValues: FieldValues | Promise<FieldValues>

    The defaultValues prop populates the entire form with default values. It supports both synchronous and asynchronous assignment of default values. While you can set an input's default value using defaultValue or defaultChecked (as detailed in the official React documentation), it is recommended to use defaultValues for the entire form.

    // set default value sync
    useForm({
        defaultValues: {
            firstName: '',
            lastName: ''
        }
    })
    
    // set default value async
    useForm({
        defaultValues: async () => fetch('/api-endpoint');
    })

    Rules

    • You should avoid providing undefined as a default value, as it conflicts with the default state of a controlled component.
    • defaultValues are cached. To reset them, use the reset API.
    • defaultValues will be included in the submission result by default.
    • It's recommended to avoid using custom objects containing prototype methods, such as Moment or Luxon, as defaultValues.
    • There are other options for including form data:
      // include hidden input
      <input {...register("hidden")} type="hidden" />
      register("hidden", { value: "data" })
      
      // include data onSubmit
      const onSubmit = (data) => {
          const output = {
              ...data,
              others: "others"
          }
      }
  • values: FieldValues

    The values props will react to changes and update the form values, which is useful when your form needs to be updated by external state or server data.

    // set default value sync
    function App({ values }) {
        useForm({
            values  // will get updated when values props updates       
        })
    }
    
    function App() {
        const values = useFetch('/api');
        
        useForm({
            defaultValues: {
                firstName: '',
                lastName: '',
            },
            values, // will get updated once values returns
        })
    }
  • resetOptions: KeepStateOptions

    This property is related to value update behaviors. When values or defaultValues are updated, the reset API is invoked internally. It's important to specify the desired behavior after values or defaultValues are asynchronously updated. The configuration option itself is a reference to the reset method's options.

    // by default asynchronously value or defaultValues update will reset the form values
    useForm({ values })
    useForm({ defaultValues: async () => await fetch() })
    
    // options to config the behaviour
    // eg: I want to keep user interacted/dirty value and not remove any user errors
    useForm({
        values,
        resetOptions: {
            keepDirtyValues: true, // user-interacted input will be retained
            keepErrors: true, // input errors will be retained with value update
        }
    })
  • criteriaMode: firstError | all

    | • When set to firstError (default), only the first error from each field will be gathered. • When set to all, all errors from each field will be gathered. | --- | | --- | --- |

  • shouldFocusError: boolean = true

    When set to true (default), and the user submits a form that fails validation, focus is set on the first field with an error.

    Note: only registered fields with a ref will work. Custom registered inputs do not apply. For example: register('test') // doesn't work

    Note: the focus order is based on the register order.


  • delayError: number

    This configuration delays the display of error states to the end-user by a specified number of milliseconds. If the user corrects the error input, the error is removed instantly, and the delay is not applied.

  • resolver: Resolver

    This function allows you to use any external validation library such as YupZodJoiVestAjv and many others. The goal is to make sure you can seamlessly integrate whichever validation library you prefer. If you're not using a library, you can always write your own logic to validate your forms.

    npm install @hookform/resolvers
### **Props**

| Name | Type | Description |
| --- | --- | --- |
| `values` | `object` | This object contains the entire form values. |
| `context` | `object` | This is the `context` object which you can provide to the `useForm` config. It is a mutable `object` that can be changed on each re-render. |
| `options` | `{   criteriaMode: string, fields: object, names: string[] }` | This is the option object containing information about the validated fields, names and `criteriaMode` from `useForm`. |

### **Rules**

- Schema validation focuses on field-level error reporting. Parent-level error checking is limited to the direct parent level, which is applicable for components such as group checkboxes.
- This function will be cached.
- Re-validation of an input will only occur one field at time during a user’s interaction. The lib itself will evaluate the `error` object to trigger a re-render accordingly.
- A resolver can not be used with the built-in validators (e.g.: required, min, etc.)
- When building a custom resolver:
    - Make sure that you return an object with both `values` and `errors` properties. Their default values should be an empty object. For example: `{}`.
    - The keys of the `error` object should match the `name` values of your fields.

### **Examples**

```jsx
import React from 'react';
import { useForm } from 'reacty-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from "yup";

const schema = yup.object().shape({
    name: yup.string().required(),
    age: yup.number().required(),
}).required();

const App = () => {
    const { register, handleSubmit } = useForm({
        resolver: yupResolver(schema),
    });

    return (
        <form onSubmit={handleSubmit(d => console.log(d))}>
        <input {...register("name")} />
        <input type="number" {...register("age")} />
        <input type="submit" />
        </form>
    );
};
```

Return

The following list contains reference to useForm return props.

  • register
  • formState
  • handleSubmit
  • reset
  • resetField
  • setError
  • clearErrors
  • setValue
  • setFocus
  • getValues
  • getFieldState
  • trigger
  • control

useController

This custom hook powers the Controller. It's useful for creating reusable Controlled input.

Props

The following table contains information about the arguments for useController.

NameTypeRequiredDescription
nameFieldPathUnique name of your input.
formUseFormReturnRequired if you haven’t wrapped your form in FormProviderform object is the value calling useForm returns. Optional when using FormProvider.
defaultValueunknownImportant: Can not apply undefined to defaultValue or defaultValues at useForm. • You need to either set defaultValue at the field-level or useForm's defaultValuesundefined is not a valid value. • If your form will invoke reset with default values, you will need to provide useForm with defaultValues.
setValueAs(value: any) => FieldPathValue<TFieldValues, TName>;Return input value by running through the function. Useful in cases like when you want to convert the number input value to number from string.

The following table contains information about properties which useController produces.

Object NameNameTypeDescription
fieldonChange(value: any) => voidA function which sends the input's value to the library. It should be assigned to the onChange prop of the input and value should not be undefined. This prop update formState and you should avoid manually invoke setValue or other API related to field update.
fieldonBlur() => voidA function which sends the input's onBlur event to the library. It should be assigned to the input's onBlur prop.
fieldvalueunknownThe current value of the controlled component.
fieldnamestringInput's name being registered.
fielddisabledbooleanWhether the form is disabled or the field is disabled.
fieldrefA ref used to connect hook form to the input. Assign ref to component's input ref to allow hook form to focus the error input.
fieldStateinvalidbooleanInvalid state for current input.
fieldStateisTouchedbooleanTouched state for current controlled input.
fieldStateisDirtybooleanDirty state for current controlled input.
fieldStateerrorobjecterror for this specific input.
formStateisDirtybooleanSet to true after the user modifies any of the inputs.Important: Make sure to provide all inputs' defaultValues at the useForm, so hook form can have a single source of truth to compare whether the form is dirty.const { formState: { isDirty, dirtyFields }, setValue } = useForm({ defaultValues: { test: "" } }); // isDirty: truesetValue('test', 'change')// isDirty: false because there getValues() === defaultValuessetValue('test', '') • File typed input will need to be managed at the app level due to the ability to cancel file selection and FileList object.
formStatedirtyFieldsobjectAn object with the user-modified fields. Make sure to provide all inputs' defaultValues via useForm, so the library can compare against the defaultValues. Important: Make sure to provide defaultValues at the useForm, so hook form can have a single source of truth to compare each field's dirtiness.• Dirty fields will not represent as isDirty formState, because dirty fields are marked field dirty at field level rather the entire form. If you want to determine the entire form state use isDirty instead.
formStatetouchedFieldsobjectAn object containing all the inputs the user has interacted with.
formStatedefaultValuesobjectThe value which has been set at useForm's defaultValues or updated defaultValues via reset API.
formStateisSubmittedbooleanSet to true after the form is submitted. Will remain true until the reset method is invoked.
formStateisSubmitSuccessfulbooleanIndicate the form was successfully submitted without any runtime error.
formStateisSubmittingbooleantrue if the form is currently being submitted. false otherwise.
formStateisLoadingbooleantrue if the form is currently loading async default values. Important: this prop is only applicable to async defaultValues const { formState: { isLoading } } = useForm({ defaultValues: async () => fetch('/api') });
formStatesubmitCountnumberNumber of times the form was submitted.
formStateisValidbooleanSet to true if the form doesn't have any errors.setError has no effect on isValid formState, isValid will always derived via the entire form validation result.
formStateisValidatingbooleanSet to true during validation.
formStateerrorsobjectAn object with field errors. There is also an ErrorMessage component to retrieve error message easily.

Example

import { TextField } from "@material-ui/core";
import { useController, useForm } from "reacty-form";

function Input({ form, name }) {
  const {
    field,
    fieldState: { invalid, isTouched, isDirty },
    formState: { touchedFields, dirtyFields }
  } = useController({
    name,
    form,
    rules: { required: true },
  });

  return (
    <TextField 
      onChange={field.onChange} // send value to hook form 
      onBlur={field.onBlur} // notify when input is touched/blur
      value={field.value} // input value
      name={field.name} // send down the input name
      inputRef={field.ref} // send input ref, so we can focus on input when error appear
    />
  );
}

Tips

  • It's important to be aware of each prop's responsibility when working with external controlled components, such as MUI, AntD, Chakra UI. Its job is to spy on the input, report, and set its value.
    • onChange: send data back to hook form
    • onBlur: report input has been interacted (focus and blur)
    • value: set up input initial and updated value
    • ref: allow input to be focused with error
    • name: give input an unique name

      It's fine to host your state and combined with useController.

      const { field } = useController();
      const [value, setValue] = useState(field.value);
      
      onChange={(event) => {
        field.onChange(parseInt(event.target.value)) // data send back to hook form
        setValue(event.target.value) // UI state
      }}
  • Do not register input again. This custom hook is designed to take care of the registration process.
    const { field } = useController({ name: 'test' })
    
    <input {...field} /> // ✅
    <input {...field} {...register('test')} /> // ❌ double up the registration
  • It's ideal to use a single useController per component. If you need to use more than one, make sure you rename the prop. May want to consider using Controller instead.
    const { field: input } = useController({ name: 'test' })
    const { field: checkbox } = useController({ name: 'test1' })
    
    <input {...input} />
    <input {...checkbox} />

Controller

Reacty-form embraces uncontrolled components and native inputs, however it's hard to avoid working with external controlled component such as React-SelectAntD and MUI. This wrapper component will make it easier for you to work with them.

Props


The following table contains information about the arguments for Controller.

NameTypeRequiredDescription
nameFieldPathUnique name of your input.
formUseFormReturnform object is the value calling useForm returns. Optional when using FormProvider.
renderFunctionThis is a render prop. A function that returns a React element and provides the ability to attach events and value into the component. This simplifies integrating with external controlled components with non-standard prop names. Provides onChangeonBlurnameref and value to the child component, and also a fieldState object which contains specific input state.
componentReact.FCIf you don’t have any customizations required you can directly pass the component in this prop instead of using render method.
componentPropsobjectThis object contain the props that you want to pass in the component you passed in component prop
defaultValueunknownImportant: Can not apply undefined to defaultValue or defaultValues at useForm.• You need to either set defaultValue at the field-level or useForm's defaultValuesundefined is not a valid value.• If your form will invoke reset with default values, you will need to provide useForm with defaultValues.• Calling onChange with undefined is not valid. You should use null or the empty string as your default/cleared value instead.
disabledboolean = falsedisabled prop will be returned from field prop. Controlled input will be disabled and its value will be omitted from the submission data.

Example

import ReactDatePicker from "react-datepicker"
import { TextField } from "@material-ui/core"
import { useForm, Controller } from "reacty-form"

type FormValues = {
  ReactDatepicker: string
}

function App() {
  const form = useForm<FormValues>()

  return (
    <form onSubmit={form.handleSubmit((data) => console.log(data))}>
      <Controller
        form={form}
        name="ReactDatepicker"
        render={({ field: { onChange, onBlur, value, ref } }) => (
          <ReactDatePicker
            onChange={onChange} // send value to hook form
            onBlur={onBlur} // notify when input is touched/blur
            selected={value}
          />
        )}
      />

      <input type="submit" />
    </form>
  )
}

useFormContext

This custom hook allows you to access the form context. useFormContext is intended to be used in deeply nested structures, where it would become inconvenient to pass the context as a prop.

Return


This hook will return all the useForm return methods and props.

const methods = useForm()

<FormProvider {...methods} /> // all the useForm return props

const methods = useFormContext() // retrieve those props

RULES

You need to wrap your form with the FormProvider component for useFormContext to work properly.

Example

import React from "react"
import { useForm, FormProvider, useFormContext } from "reacty-form"

export default function App() {
  const form = useForm()
  const onSubmit = (data) => console.log(data)
  const { register, reset } = form

  useEffect(() => {
    reset({
      name: "data",
    })
  }, [reset]) // ❌ never put `methods` as the deps

  return (
    <FormProvider form={form}>
      {/* pass all methods into the context */}
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <NestedInput />
        <input {...register("name")} />
        <input type="submit" />
      </form>
    </FormProvider>
  )
}

function NestedInput() {
  const { register } = useFormContext() // retrieve all hook methods
  return <input {...register("test")} />
}

FormProvider

This component will host context object and allow consuming component to subscribe to context and use useForm props and methods.

Props


This following table applied to FormProvideruseFormContext accepts no argument.

NameTypeDescription
formUseFormReturnFormProvider requires all useForm methods. Just pass the whole form object that useForm returns

Example

import React from "react"

import { useForm, FormProvider, useFormContext } from "reacty-form"

export default function App() {
  const form = useForm()

  const onSubmit = (data) => console.log(data)
  const { register, reset } = form

  useEffect(() => {
    reset({
      name: "data",
    })
  }, [reset]) // ❌ never put `methods` as the deps

  return (
    <FormProvider form={form}>
      {/* pass all methods into the context */}
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <NestedInput />
        <input {...register("name")} />
        <input type="submit" />
      </form>
    </FormProvider>
  )
}

function NestedInput() {
  const { register } = useFormContext() // retrieve all hook methods

  return <input {...register("test")} />
}

useWatch

It allows you to subscribe to any changes that happen in a particular form field.

Props (the first parameter of the useWatch hook)


NameTypeDescription
formUseFormReturnform object provided by useForm. It's optional if you are using FormProvider.
namestringThe name of the form field

Callback (the second parameter of the useWatch hook)

callback: (e: ObserveEventCallback<TFieldValues>) => void

  • This callback is where you write the logic for handling changes to the form field's value.
  • ObserveEventCallback comes from legend-state. This hook internally uses useObserve hook of legend state to subscribe for state changes.

useFormState

This custom hook returns the current state of the form.

Props

NameTypeDescription
formUseFormReturnform object provided by useForm. It's optional if you are using FormProvider.

Return

NameTypeDescription
isDirtybooleanSet to true after the user modifies any of the inputs. Important: Make sure to provide all inputs' defaultValues at the useForm, so hook form can have a single source of truth to compare whether the form is dirty. const { formState: { isDirty, dirtyFields }, setValue,} = useForm({ defaultValues: { test: \"\" } }); isDirty: truesetValue('test', 'change') isDirty: false because there getValues() === defaultValuessetValue('test', '') File typed input will need to be managed at the app level due to the ability to cancel file selection and FileList object.
dirtyFieldsobjectAn object with the user-modified fields. Make sure to provide all inputs' defaultValues via useForm, so the library can compare against the defaultValues. Important: Make sure to provide defaultValues at the useForm, so hook form can have a single source of truth to compare each field's dirtiness. Dirty fields will not represent as isDirty formState, because dirty fields are marked field dirty at field level rather the entire form. If you want to determine the entire form state use isDirty instead.
touchedFieldsobjectAn object containing all the inputs the user has interacted with.
defaultValuesobjectThe value which has been set at useForm's defaultValues or updated defaultValues via reset API.
isSubmittedbooleanSet to true after the form is submitted. Will remain true until the reset method is invoked.
isSubmitSuccessfulbooleanIndicate the form was successfully submitted without any runtime error.
isSubmittingbooleantrue if the form is currently being submitted. false otherwise.
isLoadingbooleantrue if the form is currently loading async default values. Important: this prop is only applicable to async defaultValues const { formState: { isLoading } } = useForm({ defaultValues: async () => await fetch('/api') });
submitCountnumberNumber of times the form was submitted.
isValidbooleanSet to true if the form doesn't have any errors. setError has no effect on isValid formState, isValid will always derived via the entire form validation result.
isValidatingbooleanSet to true during validation.
validatingFieldsbooleanCapture fields which are getting async validation.
errorsobjectAn object with field errors. There is also an ErrorMessage component to retrieve error message easily.
disabledbooleanSet to true if the form is disabled via the disabled prop in useForm.

Example

import * as React from "react";
import { useForm, useFormState } from "reacty-form";

function Child({ form }) {
  const { dirtyFields } = useFormState({
    form
  });

  return dirtyFields.firstName ? <p>Field is dirty.</p> : null;
};

export default function App() {
  const form = useForm({
    defaultValues: {
      firstName: "firstName"
    }
  });
  const { register, handleSubmit } = form;
  const onSubmit = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName")} placeholder="First Name" />
      <Child form={form} />

      <input type="submit" />
    </form>
  );
}

useFieldArray

Custom hook for working with Field Arrays (dynamic form). The motivation is to provide better user experience and performance.

Props

NameTypeRequiredDescription
namestringName of the field array. Note: Do not support dynamic name.
formUseFormReturnform object is the value calling useForm returns. Optional when using FormProvider.

Example

function FieldArray() {
  const form = useForm();
  const { fields, append, prepend, remove, swap, move, insert } = useFieldArray({
    form, // form props comes from useForm (optional: if you are using FormProvider)
    name: "test", // unique name for your Field Array
  });

  return (
    {fields.map((field, index) => (
      <input
        key={index}
        {...form.register(`test.${index}.value`)} 
      />
    ))}
  );
}

Return

NameTypeDescription
fieldsobject & { id: string }This object contains the defaultValue and key for your component.
append(obj: object \| object[], focusOptions) => voidAppend input/inputs to the end of your fields and focus. The input value will be registered during this action.Important: append data is required and not partial.
prepend(obj: object \| object[], focusOptions) => voidPrepend input/inputs to the start of your fields and focus. The input value will be registered during this action.Important: prepend data is required and not partial.
insert(index: number, value: object \| object[], focusOptions) => voidInsert input/inputs at particular position and focus.Important: insert data is required and not partial.
swap(from: number, to: number) => voidSwap input/inputs position.
move(from: number, to: number) => voidMove input/inputs to another position.
update(index: number, obj: object) => voidUpdate input/inputs at a particular position, updated fields will get unmounted and remounted. If this is not desired behavior, please use setValue API instead.Important: update data is required and not partial.
replace(obj: object[]) => voidReplace the entire field array values.
remove(index?: number \| number[]) => voidRemove input/inputs at particular position, or remove all when no index provided.

Mentions

  • I want to mention legend state for such a beautiful library which handles state management beautifully
  • I also want to mention react-hook-form from which I took so much inspiration while building this library.

Support

If you enjoy using this project or want to help improve it, your support means the world! You can:

  • ⭐ Star the repository
  • 🗨️ Share feedback

License

This project is licensed under the MIT License.