1.0.26 • Published 8 months ago

@rvision/use-form v1.0.26

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

useForm

React forms hook, lightweight alternative to existing frameworks.

See full demo

Concerned about performance? Try this demo, optimized with with React.memo:

400 form inputs

Installation

npm install --save @rvision/use-form

or

yarn add @rvision/use-form

Quickstart: basic usage

const defaultValues = {
  firstName: '',
  lastName: '',
  email: '',
  agree: false
};

const Form = () => {
  const {
    register,
    handleSubmit,
  } = useForm({
    defaultValues
  });

  const onSubmit = values =>  console.log(values); // handles form submit: call API, etc.

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
        <label>
          Enter first name:
          <input type="text" {...register('firstName')} />
        </label>
        <label>
          Enter last name:
          <input type="text" {...register('lastName')} />
        </label>
        <label>
          Enter email:
          <input type="email" {...register('email')} />
        </label>
        <label>
          <input type="checkbox" {...register('agree')} />
          I agree to terms and conditions
        </label>
        <button type="submit">
          Register
        </button>
    </div>
  );
}

Another form library?

Recently, react-hook-form gained stellar popularity because it's easy to use and it uses uncontrolled components, making it very performant. And it works fine. Unless you have to deal with complex, dynamic forms, then you'll have to use formState, control, Controller, useController, useFormContext, watch, useWatch, useFormState, useFieldArray and on and on. This imperative not to "re-render" form component ended up in having multiple useEffects and derived states which makes your code harder to reason about. Removing unnecessary Effects will make your code easier to follow, faster to run, and less error-prone..

Motivation

I ended building this library because the code for the complex forms was absolute horror. This library works with controlled components only, meaning that on each keystroke in input field, whole form will be re-rendered. Performance depends on the number and types of components used. For native inputs, it is decently fast, for custom components, your mileage may vary. Note that handlers have stable references. This allows you to memoize parts of big forms and achieve better rendering performance. If you have hundreds of inputs in your form, you're not building a form, you are building Excel clone an you should probably use uncontrolled inputs.

Goals

  • 0 dependencies
  • lightweight: ~3.5kb minified & gzipped
  • simplicity: low learning curve
  • nested arrays without hassle
  • un-opinionated - components freedom: doesn't force you to use any specific component for inputs or form, it allows use of native input fields via register and custom components via getValue/setValue functions
  • stable handlers allow easily to memoize rendering
  • schema-based validation: sync validation support for yup and zod included
  • natural way to reference to any field with regards of the initial object shape/structure. For example:
getValue('firstName') // 1st level property
setValue('birthDate') // 1st level property
getValue('departments.5.employees.3.tasks.1.name')	// nested property, name of second task
trigger('cities.2.population')	// nested property, population of third city
getValue('artists.1.albums.2')	// nested property, third album of the second artist

Let's call this identifier fullPath. Most of the methods to work with fields and errors will use this naming convention for operations.

Hook options

useForm({
	id: '',
	defaultValues: {},
	mode: 'onSubmit',
	classNameError: null,
	shouldFocusError: false,
	resolver: () => {}
});
PropertyTypeDescription
idstringidentifier: set it only if you have multiple forms at once (used internally for stable callbacks)
defaultValuesobjectinitial form values; for new records it has to be populated with default values (e.g. empty strings, true/false, date, etc.)
mode'onSubmit' / 'onChange' / 'onBlur'validation mode, see below
classNameErrorstringRegistered fields with validation error will have this css class name appended to their className list. Errors and Error components will use this class name also
bashouldFocusErrorboolif field has validation error, it will focus on error field when validation mode is onBlur. Also, when form is submitted and there are errors, it will focus on the first field with validation error
resolverfunction(fieldValues)validation function, yupResolver and zodResolver are provided for yup and zod or you can write your custom

Validation

ModeDescription
default validaton behaviour: validates when form is submitted, if there are any validation errors - when edited, fields with errors will be re-validated like 'onChange' mode. Like a combination of 'onSubmit' and 'onChange' modes. I find this pattern suitable for most use cases.
'onSubmit'validates when form is submitted, if there are any validation errors - when edited, errors will be removed for fields with errors and revalidated on next form submit
'onChange'validates form fields on each change
'onBlur'validates fields when focused out. For registered inputs, it will trap user to the field if bashouldFocusError option is set to true

Returned props

const {
	register,
	getValue,
	setValue,
	onChange,
	onBlur,
	getRef,
	setRef,
	trigger,
	handleSubmit,
	hasError,
	getError,
	clearError,
	setErrors,
	array: {
		clear,
		append,
		prepend,
		remove,
		swap,
		insert,
	},
	key,
	Error,
	Errors,
	formState: {
		errors,
		isValid,
		isTouched,
		isDirty,
		hadError,
		reset,
	},
} = useForm(options);

register(fullPath: string, className: string )

field registration method for native inputs; uses fullPath concept to identify the field in the object hierarchy; className is used with classNameError setting if field has validation error

{/* Examples: */}
<input type="text" {...register('movies[${i}].coStars[${j}].firstName')} />
{/* or */}
<input type="checkbox" {...register('agreeToTermsAndConditions')} />
{/* or */}
<input type="number" {...register('employee[5].age', 'my-number-input' )} />
{/* or */}
<input type="radio" {...register('radio')} checked={String(getValue('radio')) === String(option.id)} value={option.id} />
{/* or */}
<select {...register('cars[2].stereo')}>
	<option key="mp3" value="mp3">MP3 player</option>
	<option key="cd" value="cd">CD player</option>
</select>

NOTE Only input type that doesn't allow automatic registration is file type, because of the browser security. You can only use onChange event to set the value, but you cannot display it. Here is an example how to deal with those fields:

<div className="field-body">
	{getValue('files').map((file, idx) => {
		return (
			<React.Fragment key={key(file || key())}>
				<div className="file">
					<label className="file-label">
						<input className="file-input" name={`files[${idx}]`} type="file" onChange={onChange} />
						<span className="file-cta">
							<span className="file-label">{(file || {}).name || 'Choose a file…'}</span>
						</span>
					</label>
				</div>
				<Error for={`files[${idx}]`} />
			</React.Fragment>
		);
	})}
</div>

getValue(fullPath: string) : any

method to get value of the field, uses fullPath concept as field identifier. use it for custom components

<ReactDatePicker selected={getValue('birthDate')} />

setValue(fullPath: string, newValue: any, shouldRevalidate: true)

method to set value of the field, uses fullPath concept as field identifier. promise returns new values/errors object. use it for custom components.

<ReactDatePicker
	onChange={date => {
		setValue('movies.3.releaseDate', date, false);
	}}
/>
{/* or */}
<a onClick={setValue('movies',[])}>Clear movie list</a>{/* NOTE:use array.clear if you want to preserve existing validation errors */}

onChange(event: React.SyntheticEvent)

if you need debouncing or additional logic when field value is changed, use 'onChange' method; it overrides default method set by register

<input
	type="text"
	{...register('firstName')}
	onChange={e => {
		// additional logic
		onChange(e);
	}}
/>

onBlur(event: React.SyntheticEvent)

same, but for onBlur event

<input
	type="text"
	{...register('firstName')}
	onBlur={e => {
		//additional logic
		onBlur(e);
	}}
/>

getRef(fullPath: string)

helper method to get reference (ref) to the native input, uses fullPath concept as field identifier

<a onClick={() => {
	getRef(`movies[${i}].coStars[0].firstName`).focus()
}}>

setRef(fullPath: string, element: ref-able DOM element)

helper method to store reference (ref) to the native input, uses fullPath concept as field identifier; Use it for storing refs for custom components, this way they can be focusable when clicking on the error in the list of errors from <Errors /> component

<ReactDatePicker
	selected={getValue('birthDate')}
	ref={ref => {
		if (ref && ref.input) {
			setRef('birthDate', ref.input);
		}
	}}
	onChange={date => {
		setValue('birthDate', date);
	}}
/>
{/* or */}
<Select
	placeholder="Select Genre(s)"
	ref={ref => setRef(`movies.${idx}.genres`, ref)}
	onChange={options => {
		setValue(`movies.${idx}.genres`, options);
	}}
/>

hasError(fullPath: string?, targetErrors: any?) : bool

returns bool if field value has validation errors; if fullPath is omitted returns if there are any errors for all fields

{hasError('firstName') && <p>This field has errors</p>}

getError(fullPath: string?, targetErrors: any?) : any

returns error object (type/message) if field value has validation errors; if fullPath is omitted returns errors for all fields

{hasError('firstName') && <p>{getError('firstName').message}</p>}

clearError(fullPath: string)

clears error from the errors object for specific fullPath

<a onClick={clearError('firstName')}>Clear firstname validation error</a>

setErrors(errors: object)

allows to set validation errrors from other parts of the system (e.g. API or similar)

setErrors({
	email: {
		type: 'duplicate',
		message: 'This email address is already in use',
	},
	['otherFields[2].otherProperty']: {
		type: 'invalid',
		message: 'This property is invalid',
	},
});

trigger(fullPath: string? | string[], newValues: any?) : Promise<{errors: any, values: any}>

triggers validation on default form values object or passed newValues; it re-validates only the error with fullPath; returns errors in promise

<a onClick={trigger('firstName').then(errors => console.log(errors))}>Check first name</a>

Error

React component to display field validation error, can be used with render props or can be wrapped to customize the error markup; visible only if there is a validation error; if set, uses classNameError from the options

<Error for="movies[3].actors[0].firstName">{({ type, message }) => <p className="my-custom-css-class">{message}</p>}</Error>
{/* or */}
<Error for="firstName" />	// => this will render <span className="required classNameError">First name is required</span>

Errors

React component that renders validation summary with all validation errors as <li />. If focusable, each of the errors will behave like a link when clicked and focus on the input with error. Can be used with render prop or without.

<Errors focusable>
	{errorList => (
		<div className="notification is-danger">
			<ul className="validation-errors">{errorList}</ul>
		</div>
	)}
</Errors>
{/* or */}
<Errors focusable={false} />	// => will render <li>Please enter first name</li>... etc.

handleSubmit(handler: function) : bool

performs validation first; prevents default event; submits form data to handler function; focuses on first field with error (if errors exist and bashouldFocusError is set to true); returns true if form was submitted, otherwise false

<button type="submit" onClick={handleSubmit(values => console.log(values))}>
	Save the form
</button>
{/* or */}
<form onSubmit={handleSubmit(onSubmit)}>
	<button type="submit">Submit</button>
</form>

key(any?): string

helper method to get unique keys for siblings when rendering arrays. It works by utilizing WeakMap where objects are keys. Can be used to generate unique id's as well, by omitting the object parameter (e.g. key()). It keeps same keys even for new objects, modified via setValue

NOTE: it is advised to use stable keys, so if you have database uuid, give it a priority:

{getValue(`movies[${i}].coStars`).map((star) => {
	return (
		<li key={star.id || key(star)}>
			...other markup
		</li>
	)
}

Methods to work with arrays

NOTE: Pass reValidate parameter as true if you want to trigger validation (if set mode requires it (onSubmit, onChange, onBlur)), otherwise false. This allows flexibility in behaviour, e.g. if you are adding an element to array which already has error on let's say 1st element, but limit of the array size is 2

array.clear(fullPath: string)

clears existing array

<button onClick={e => {
	e.preventDefault();
	array.clear('albums');
}}>Clear albums</button>

array.append(fullPath: string, newValue: any)

appends new object to the existing array of objects

<button onClick={e => {
	e.preventDefault();
	array.append('albums', { name: '', releaseDate: null });
}}>Add new album</button>

array.prepend(fullPath: string, newValue: any)

prepends new object to the existing array of objects

<button onClick={e => {
	e.preventDefault();
	array.prepend('movies[3].actors', { name: '' });
}}>Add new actor</button>

array.remove(fullPath: string, index: int)

removes array item from the existing array

<button onClick={e => {
	e.preventDefault();
	array.remove('movies[3].actors', 3);
}}>Remove this actor</button>

array.swap(fullPath: string, index1: int, index2: int)

swaps item positions in the existing array, useful for drag'n'drop operations

<button onClick={e => {
	e.preventDefault();
	array.swap('movies[3].actors', 0, 1);
}}>Swap actors</button>

array.insert(fullPath: string, index: int, newValue: any)

inserts item to the array at specific index

<button onClick={e => {
	e.preventDefault();
	array.insert('movies[3].actors', { firstName: 'James', lastName: 'Dean' }, 5);
}}>Swap actors</button>

Form state props

These props return current form state and reset function

formState: {
	errors: object,
	isValid: bool,
	isTouched: bool,
	isDirty: bool
	hadError: bool,
	reset: (values?: any, reValidate?: boolean) => void;
}

errors: object

object with validation errors; shape of this object follows the shape/structure of form values

isValid: bool

true if there are no validation errors; when mode is 'onSubmit', errors and this field will be populated when form is submitted

isTouched: bool

true if any of the fields are changed

isDirty: bool

true if any of form values is different than default ones provided

hadError: bool

true if any of form data changes produced validation errors

reset(newValues: any?, reValidate: bool)

resets the form to newValues or default ones; triggers revalidation based on reValidate parameter

<button type="button" onClick={formState.reset}>
	Reset the form
</button>

Contributions

PRs welcome

Roadmap/plan

  • target size: below 4kb
  • no async validation currently
  • no fields validation code will be introduced; it's pointless since there are alredy a bunch of libraries that do that

Changelog

<= v1.0.24:

  • fixed number detection
  • refactored trigger
  • removed promises and revalidate flag form array operations
  • reset form fix
  • exports getError
  • _deleteNested fix
  • removed cloning of initial values
  • introducing Promises for trigger and setValue
  • react & react-dom: same versions in dev and peer dependencies
  • fix for null defaultValues, minor improvements
  • setValue returns new values; trigger triggers validation for whole object if fullPath is undefined
  • trigger accepts multiple fullPaths; defaultValues doesn't create new instances when null is passed; setCustomErrors use useEvent
  • added useEvent for stable handler references; fix yupResolver export
  • register method does not accept object as second parameter (className), but plain string

License

MIT

Free Software, Hell Yeah!

1.0.26

8 months ago

1.0.22

1 year ago

1.0.25

11 months ago

1.0.24

11 months ago

1.0.23

12 months ago

1.0.19

1 year ago

1.0.18

1 year ago

1.0.21

1 year ago

1.0.20

1 year ago

1.0.17

1 year ago

1.0.16

2 years ago

1.0.11

2 years ago

1.0.10

2 years ago

1.0.15

2 years ago

1.0.14

2 years ago

1.0.13

2 years ago

1.0.12

2 years ago

1.0.9

2 years ago

1.0.8

2 years ago

1.0.7

2 years ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.0.3

2 years ago

1.0.2

3 years ago

1.0.1

3 years ago

1.0.0

3 years ago