react-lightweight-forms is a simple to use, lightweight forms API based on React hooks. It only provides the functional aspects of forms. It's up to you to develop your UI as you see fit, and simply integrate your UI with the hooks.
This library supports the following:
- Form state management
- Form field validations
- Form submission
- Form field state management
- Doesn't require you to develop functional React UI components - you can continue to use your class based components
ThoroughlySomewhat unit tested 😃- Supports asynchronous validations on blur, on change, and on submit
- Supports standard HTML inputs such as:
- Text inputs (input type text, textareas)
- Radio button groups
- Checkboxes
- Multi-selects
- Supports custom components
- Supports custom validations
- Supports yup-based schema validations
- Inputs support pristine and visited state
- Dynamically add or remove form fields to/from existing forms
- Easy to get started!
See my multi-part article series for background. I took it upon myself to develop this forms library as a part of learning a bit about React hooks since I believe that hooks are a better way to develop React components.
See complete demo of all features.
This is not an NPM package at this time. Two install options you can:
npm install --save https://github.com/shanplourde/react-hooks-form-util#master- Copy the source code under
src/components/forminto your project and run with it
Call useForm and pass the form id along with your form's initial state.
useFormreturns your form's current state under theinputValuesobject.useFormreturnsgetFormProps, which you need to expand onto yourformtag- Your
onSubmitgets called by theuseFormhook, via thegetFormPropsthat you expand onto your form tag
import { useForm } from "form/use-form";
const { inputValues, getFormProps } = useForm({
id: "settingsForm",
initialState: {
firstName: "George",
lastName: "OfTheJungle",
email: "george@thejungle.com",
custom: "custom",
agreeToTerms: false,
comments: "",
favouriteFlavour: "",
favouriteColours: ["red", "green"],
cookiesPerDay: null,
preferredDate: null
}
});
console.log(inputValues.firstName); // George
///
const onSubmit = async ({ evt, inputValues }) => {
await sleep(2000);
console.log("onSubmit was called", inputValues);
};
///
<form {...getFormProps({ onSubmit })}>useForm returns an API that you can use to configure your form's inputs.
- Call
api.addInputto define your input - Call
api.addRadioGroupto add a radio button group (this will probably be rolled intoapi.addInput). - Expand
getInputPropsonto your inputs
import { useForm } from "form/use-form";
const { inputValues, api } = useForm({
id: "settingsForm",
initialState: {
firstName: "George",
lastName: "OfTheJungle",
email: "george@thejungle.com",
custom: "custom",
agreeToTerms: false,
comments: "",
favouriteFlavour: "",
favouriteColours: ["red", "green"],
cookiesPerDay: null,
preferredDate: null
}
});
const firstNameInput = api.addInput({
id: "firstName",
value: inputValues.firstName
});
//
<div className="field-group">
<label htmlFor={firstNameInput.id}>
First name {JSON.stringify(inputUiState.firstName)} --{" "}
{JSON.stringify(formValidity.firstName)} *
</label>
<input type="text" {...firstNameInput.getInputProps()} />
- Call
api.removeInputto remove an existing input field - Removing an input field removes all state associated with the input, such validation state, input value, etc.
- The demo/dynamic-form example shows how to add and remove form fields dynamically to an already rendered form
/// Add your inputs, say an input with id = firstName
api.removeInput("firstName");- Pass a
validatorsarray toapi.addInput - See
validators.jsfor out of the box validations (currentlyrequired,email,mustBeTrue,schema) - For each validator, specify the
whenarray, which indicates when validators fire. Validators can be firedonBlur,onSubmit, andonChange - Validity state is not set for form fields that don't specify validity
- Validity state is only set once a validation has run
useFormreturns aformValidityobject. Each key is an input id. Each value is an array of validation errors
import { validators, validateInputEvents } from "validators";
const { required, email } = validators;
const { onBlur, onSubmit, onChange } = validateInputEvents;
const { formValidity, inputValues, api } = useForm({
...
});
const firstNameInput = api.addInput({
id: "firstName",
value: inputValues.firstName,
validators: [{ ...required, when: [onChange, onSubmit] }]
});
const lastNameInput = api.addInput({
id: "lastName",
value: inputValues.lastName,
validators: [{ ...required, when: [onBlur, onSubmit] }]
});
const emailInput = api.addInput({
id: "email",
value: inputValues.email,
validators: [
{ ...required, when: [onBlur, onSubmit] },
{
...email,
when: [onBlur, onSubmit]
}
]
});
//
console.log('formValidity', formValidity, formValidity.lastName)- Import
createValidatorand pass an object with avalidationFnproperty, representing your validation function. Your validation function receives the input value - Pass an
errorproperty, which is the key that you can use to determine what validation errors triggered for a given input
import { createValidator, validateInputEvents } from "validators";
const { onBlur, onSubmit } = validateInputEvents;
const { formValidity, inputValues, api } = useForm({
...
});
const customValidator = createValidator({
validateFn: async ({ value }) =>
await new Promise(resolve => {
setTimeout(() => {
resolve((value || "").length > 8);
}, 5000);
}),
error: "CUSTOM_ASYNC_ERROR"
});
const customInput = api.addInput({
id: "custom",
value: inputValues.custom,
validators: [{ ...customValidator, when: [onBlur, onSubmit] }]
});
console.log(formValidity.custom); // returns validity state- When creating a custom validator with
createValidator,validateFnreceives ainputValuesargument that allows you to compare a form field against all other values in the current form
import { createValidator, validateInputEvents } from "validators";
const { onBlur, onSubmit } = validateInputEvents;
const emailInput = api.addInput({
id: "email",
value: inputValues.email,
validators: [
{ ...required, when: [onBlur, onSubmit] },
{
...email,
when: [onBlur, onSubmit]
}
]
});
const confirmEmailValidator = createValidator({
validateFn: ({ value, inputValues }) =>
value === inputValues.email;
},
error: "EMAILS_DO_NOT_MATCH"
});
const confirmEmailInput = api.addInput({
id: "confirmEmail",
value: inputValues.confirmEmail,
validators: [{ ...confirmEmailValidator, when: [onBlur, onSubmit] }]
});- Nothing else needs to be done for asynchronous validations, they're supported right out of the box
- The custom validator below is one example
const customValidator = createValidator({
validateFn: async ({ value }) =>
await new Promise(resolve => {
setTimeout(() => {
resolve((value || "").length > 8);
}, 5000);
}),
error: "CUSTOM_ASYNC_ERROR"
});- Schema validations are supported with the yup schema validation library
- Schema validations are configured at the field level, meaning that you have fine-grained control over when schema validations run ( i.e. onBlur, onSubmit, or using the reward early validate late pattern)
- Checkout the code below.
mySchemais our schema definition. We pass this touseForm, and then use the predefinedschemavalidator. In addition, we get to specify thewhenproperty for our schema validations
import { object, string } from "yup";
const { schema } = validators;
const mySchema = object({
firstName: string()
.required()
.min(3),
lastName: string()
.required()
.length(10),
email: string().email()
});
const { api } = useForm({
id: "kitchenSinkForm",
initialState,
validationSchema: contactSchema
});
const firstNameInput = api.addInput({
id: "firstName",
value: inputValues.firstName,
validators: [
{
...schema,
when: [
{
eventType: onChange,
evaluateCondition: evaluateConditions.rewardEarlyValidateLate
},
onBlur,
onSubmit
]
}
]
});- When defining your
whencondition for validators, you can choose from the eventsonBlur,onChange, andonSubmit - In addition, you can define your own custom expression with
evaluateConditionthat decides if your validator should trigger - Predefined evaluateConditions:
evaluateConditions.rewardEarlyValidateLate: Returns true if theformValidityfor the given input is false
- evaluateCondition functions receive
{ id, inputValues, formValidity }asidis the given input's idinputValuesare the given form's input valuesformValidityis the current form's input validity
import { validators, validateInputEvents, evaluateConditions } from "validators";
const { required, email } = validators;
const { onBlur, onSubmit, onChange } = validateInputEvents;
const { formValidity, inputValues, api } = useForm({
...
});
// Example of using the stock evaluateConditions.rewardEarlyValidateLate
// evaluator with the onChange event:
const firstNameInput = api.addInput({
id: "firstName",
value: inputValues.firstName,
validators: [
{
...required,
when: [
{
eventType: onChange,
evaluateCondition: evaluateConditions.rewardEarlyValidateLate
},
onBlur,
onSubmit
]
}
]
});
// Example of using a custom evaluateCondition
// evaluator with the onChange event. evaluateCondition
// returns true, so the validation is always evaluated
// onChange
const firstNameInput = api.addInput({
id: "firstName",
value: inputValues.firstName,
validators: [
{
...required,
when: [
{
eventType: onChange,
evaluateCondition: ({ id, inputValues, formValidity }) => true
},
onBlur,
onSubmit
]
}
]
});- useForm does not block form submits if the form is in an invalid state
- This allows you to either submit the form on error, prevent submission, or check if critical validations passed before submission, for example
- All validations are executed before your custom
onSubmitform is invoked formValidityand uiState.isValid are set before your customonSubmitis executed
const { getFormProps, inputValues, uiState, formValidity } = useForm({
id: "settingsForm",
initialState: {
firstName: "George",
lastName: "OfTheJungle"
///
}
});
const handleOnSubmit = async ({ evt, inputValues }) => {
await sleep(2000);
console.log("sample-form onSubmit, inputValues", inputValues);
if (uiState.isValid || formValidity.firstName.isValid) {
// Guess we're ok with just first name being valid, let's submit our form
}
};
<form {...getFormProps({ onSubmit: handleOnSubmit })} />;useFormreturns auiStateproperty that allows you to track the following:isValidating: is form getting validatedisSubmitting: is form getting submittedisValid: is form valid
A submit button could therefore be disabled, or a modal overal perhaps displayed, if the form was being validated or submitted.
<div className="input-footer">
<button type="submit" disabled={uiState.isSubmitting || uiState.isValidating}>
Save
</button>
{uiState.isSubmitting || (uiState.isValidating && <p>Submitting...</p>)}
</div>useForm's api.addInput returns an input with a uiState property. You can use this to get the input's visited and pristine state.
visited is set to true whenever an input receives focus.
pristine is set to false whenever an input's value changes from its original value. pristine can be set to false, then true should the user change the input value back to its original value.
const firstNameInput = api.addInput({
id: "firstName",
value: inputValues.firstName,
validators: [{ ...required, when: [onChange, onSubmit] }]
});
console.log(firstNameInput.uiState); // { visited: false, pristine: true }- useForm doesn't handle custom onSubmit errors, you'll have to handle these
- When a custom validation throws an error, the validator's state
is set to
isValue = true, and a property calledundeterminedValidationsis set that includes the validation name (error key), and error details
- Each blur / change event on an input requests new asynchronous validations
- However if an input's value is the same, the new validation request is ignored, allowing the original validation request to complete. This should optimize the user experience
- In the future, this may be an option that you can opt out of, in case you need an asynchronous validation that depends on other form field values
- Each blur / change event on an input requests new asynchronous validations
- When the latest input value is different from the previous, a new asynchronous validation request is kicked off
- Previously ran validation requests are ignored, but any asynchronous activity they are performing is not cancelled
Feel free to open an issue to raise questions, bugs, suggestions, etc.