There are other well established solutions like Formik, React-Hook-Form, Redux Form...
This lib aims to provide the best experience for developers (DX) and users (UX) when creating
advanced forms in React (have a look at the features below).
If you feel concerned, then it's all for you :)
- Fields props initialization at form level (optional)
- Management of fields state and updates (value and onChange)
- Tracking of modified fields
- Tracking of touched fields
- Various form status info (modified, disabled, validating, submitting...)
- Form loading using promise (optional)
- Auto disabling fields until form is initialized
- Auto disabling fields when form is disabled, not modified, validating or submitting
- Parsing of field value when modified (smart typing or custom parser)
- Replacement of empty string by null on field change and form submit
- Trim values on form submit
- Field validation on change (optional)
- Field validation on init/load (optional)
- Field validation on touch (optional)
- Field validation on submit (optional)
- Field and form validation using a custom function or schema (like yup)
- Form and field errors handling
- Reset form or fields
- Handling form submission errors and retries
- Compatible with custom components libraries
- TypeScript declarations ♥
You can play with the lib here: https://codesandbox.io/s/jalik-react-form-demo-wx6hg?file=/src/components/UserForm.js
npm i -P @jalik/react-form
yarn add @jalik/react-form
import { Button, Field, Form, useForm } from '@jalik/react-form'
/**
* Authenticates by username and password.
* @param username
* @param password
*/
function authenticate (username, password) {
return fetch('https://www.mysite.com/auth', {
method: 'POST',
body: JSON.stringify({
username,
password
}),
headers: { 'content-type': 'application/json' }
})
}
function SignInForm () {
const form = useForm({
initialValues: {
username: null,
password: null
},
// onSubmit needs to return a promise,
// so the form is aware of the submit state.
onSubmit: (values) => authenticate(values.username, values.password)
})
return (
// Using the provided components allows writing code faster while keeping it very concise.
// <Field> and other components must be nested in a <Form> with the form context.
<Form context={form}>
<Field name="username" />
<Field name="password" />
<Button type="submit">Sign in</Button>
</Form>
)
}
There are several ways to load a form:
- Loading values inside or outside the form component ;
- Loading values using the
load
option ofuseForm()
;
import { Field, Form, useForm } from '@jalik/react-form'
import { useEffect } from 'react'
import { useParams } from 'react-router'
function UserFormPage () {
const params = useParams()
const [user, setUser] = useState(null)
// Load user and call setUser(user)...
const form = useForm({
// initialValues must be null (or omitted) at first,
// so the form will understand that it will be initialized later.
initialValues: user,
reinitialize: true,
onSubmit: (values) => Promise.resolve({ saved: true }),
})
return (
<Form context={form}>
<Field name="firstName" />
<Field name="lastName" />
<Button type="submit">Save</Button>
</Form>
)
}
import { Field, Form, useForm } from '@jalik/react-form'
import { useCallback } from 'react'
function loadUser (id) {
return fetch(`/api/user/${id}`).then((resp) => resp.json())
}
function UserFormPage (props) {
const params = useParams()
const form = useForm({
// initialValues must be null (or omitted) at first,
// so the form will understand that it will be initialized later.
initialValues: null,
// WARNING: load is called every time it changes,
// in this case the form will be updated when the id changes.
// Note that all fields are disabled during loading.
load: useCallback(() => loadUser(params.id), [params.id]),
onSubmit: (values) => Promise.resolve({ saved: true }),
})
return (
<Form context={form}>
<Field name="firstName" />
<Field name="lastName" />
<Button type="submit">Save</Button>
</Form>
)
}
Form validation using a schema needs a small amount of work.
Here we use @jalik/schema
to validate the form using a schema, but it is possible to use any lib (
yup, joi...).
import { Button, Field, FieldError, Form, useForm } from '@jalik/react-form'
import Schema from '@jalik/schema'
/**
* Returns field props based on schema constraints.
* @param schema
*/
export function createFieldInitializer (schema) {
// function called by initializeField
return (name) => {
const field = schema.getField(name)
return field ? {
required: field.isRequired()
} : null
}
}
/**
* Validates the field using the schema.
* @param schema
*/
export function createFieldValidator (schema) {
// function called by validateField
return async (name, value) => {
schema.getField(name).validate(value)
}
}
/**
* The function returned validates the form (all fields) using the schema.
* @param schema
*/
export function createFormValidator (schema) {
// function called by validate
return async (values) => schema.getErrors(values)
}
// Prepare the form validation schema.
const SignInFormSchema = new Schema({
username: {
type: 'string',
required: true,
minLength: 1
},
password: {
type: 'string',
required: true,
minLength: 1
}
})
const initializeField = createFieldInitializer(SignInFormSchema)
const validate = createFormValidator(SignInFormSchema)
const validateField = createFieldValidator(SignInFormSchema)
function SignInForm () {
const form = useForm({
initialValues: {
username: null,
password: null
},
// This function sets the fields props based on a schema.
initializeField,
// This function validates all fields (even missing ones) based on a schema.
validate,
// This function validates a single field based on a schema.
validateField,
onSubmit: (values) => Promise.resolve({ success: true })
})
return (
<Form context={form}>
<Field name="username" />
<FieldError name="username" />
<Field name="password" />
<FieldError name="password" />
<Button type="submit">Sign in</Button>
</Form>
)
}
It's possible to use custom UI components with provided components <Field>
, Button
.
import { Button, Field } from '@jalik/react-form'
import { Button as RsButton } from 'reactstrap'
import { TextInput } from 'mantine/core'
export function FormButton (props) {
return <Button {...props} component={RsButton} />
}
export function FormInput (props) {
return <Field {...props} component={TextInput} />
}
This is where the magic happens, this hook defines the form state and its behavior.
import { useForm } from '@jalik/react-form'
const form = useForm({
// optional, used to clear form state (values, errors...) after submit
clearAfterSubmit: false,
// optional, used to debug form
debug: false,
// optional, used to disable all fields and buttons
disabled: false,
disableOnSubmit: true,
disableOnValidate: true,
// optional, used to set initial values
initialValues: undefined,
// optional, used to replace empty string by null on change and on submit
nullify: false,
// optional, used to set field props dynamically
initializeField: (name, formState) => ({
className: formState.modifiedFields[name] ? 'input-modified' : undefined,
required: name === 'username'
}),
// optional, used to load initial values
load: () => Promise.resolve({
id: 1,
username: 'test'
}),
// REQUIRED, called when form is submitted
onSubmit: (values) => Promise.resolve({ success: true }),
// optional, called when form has been successfully submitted
onSubmitted: (result) => {
},
// optional, used to initialize form everytime initialValues changes
reinitialize: false,
// Use submitted values to set initial values after form submission, so when form is reset, the last submitted values are used.
setInitialValuesOnSuccess: false,
// optional, used to debounce submit
submitDelay: 100,
// optional, called when a field value changed
// mutation contains all pending changes in a flat object ({field: value})
// values contains the next form values
transform: (mutation, values) => {
// in this example, if lastname or firstname changed,
// we set the value of "username" like "john.c"
if (mutation.lastname || mutation.firstname) {
mutation.username = [
values.firstname,
(values.lastname || '')[0]
].join('.').toLowerCase()
}
return mutation
},
// optional, used to remove extra spaces on blur
trimOnBlur: false,
// optional, used to remove extra spaces on submit
trimOnSubmit: false,
// optional, used to validate all fields (expect a promise)
validate: async (values) => {
const errors = {}
if (!values.username) {
// error can be a string
errors.username = 'field is required'
// or an Error
errors.username = new Error('field is required')
}
return errors
},
// optional, used to debounce validation
validateDelay: 200,
// optional, used to validate a single field (expect a promise)
validateField: async (name, value, values) => {
if (name === 'username' && !value) {
// error can be a string
return 'field is required'
// or an Error
return new Error('field is required')
}
},
// optional, used to validate field on change
validateOnChange: false,
// optional, used to validate all fields on initialization
validateOnInit: false,
// optional, used to validate all fields on submit
validateOnSubmit: true,
// optional, used to validate field on touch
validateOnTouch: false
})
This hook returns the form context and functions.
import { useFormContext } from '@jalik/react-form'
const {
// clears the form (values, errors...)
clear,
// clears all errors
clearErrors,
// clears all or given fields
clearTouchedFields,
// tells if the form is disabled
disabled,
// fields errors
errors,
// returns the field props by name
getButtonProps,
// returns the field props by name
getFieldProps,
// returns the field initial value by name
getInitialValue,
// returns the field initial value by name
getValue,
// handler for onChange events
handleChange,
// handler for onBlur events
handleBlur,
// handler for onReset events
handleReset,
// handler for value based onChange events
// (value) => {} instead of (event) => {}
handleSetValue,
// handler for onSubmit events
handleSubmit,
// tells if the form has errors
hasError,
// tells if the form has been initialized
initialized,
// initial values (used when form is reset)
initialValues,
// the load function
load,
// loading error (if any)
loadError,
// tells if the form is loading
loading,
// tells if the form was modified
modified,
// the list of modified fields
modifiedFields,
// tells if the form will trigger a validation
// can be a boolean or a list of fields to validate
needValidation,
// removes fields (used for dynamic forms)
removeFields,
// resets all or given fields to their initial values
reset,
// sets a single field error
setError,
// sets fields errors
setErrors,
// sets the initial values
setInitialValues,
// set a single touched field
setTouchedField,
// set all or given touched field
setTouchedFields,
// sets value of a field
setValue,
// sets values of multiple fields
setValues,
// submits the form with values (validate first)
submit,
// validates all fields
validate,
// validates given fields
validateFields,
// the number of times the form was submitted
// resets to zero when submission succeeds
submitCount,
// the submit error (if any)
submitError,
// the submit result (returned by onSubmit)
submitResult,
// tells if the form was submitted (changes to false when form is modified)
submitted,
// tells if the form is submitting
submitting,
// tells if the form was touched
touched,
// the list of touched fields
touchedFields,
// the validation error (if any)
// happens only when an error is thrown during validation
// it's different from the field validation errors
validateError,
// tells if the form was successfully validated
validated,
// tells if a field should be validated on change
validateOnChange,
// tells if all fields should be validated on initialization
validateOnInit,
// tells if all fields should be validated on submit
validateOnSubmit,
// tells if a field should be validated on touch
validateOnTouch,
// tells if the form is validating
validating,
// the form values
values
} = useFormContext()
Some components are provided to ease forms building.
This component is synced with the form, so whenever the form is disabled (because it is loading, validating or submitting), the button disabled.
import { Button } from '@jalik/react-form'
function SubmitButton () {
return (
<Button type="submit">Submit</Button>
)
}
This component handles the field value and logic.
The name is required.
import { Field } from '@jalik/react-form'
import { Switch } from '@mantine/core'
function parseBoolean (value) {
return /^true|1$/gi.test(value)
}
export function AcceptTermsField () {
return (
<Field
component={Switch}
name="acceptTerms"
parser={parseBoolean}
type="checkbox"
value="true"
/>
)
}
export function CountryField () {
return (
<Field
name="country"
type="select"
options={[
{
label: 'French Polynesia',
value: 'pf'
},
{
label: 'New Zealand',
value: 'nz'
}
]}
/>
)
}
This component automatically displays the field error (if any).
import { Field, FieldError } from '@jalik/react-form'
export function PasswordField () {
return (
<>
<Field
name="password"
type="password"
/>
<FieldError name="password" />
</>
)
}
This component contains the form context, so any component nested in a Form
can access the form
context using useFormContext()
.
import { Button, Field, Form, useForm } from '@jalik/react-form'
export function SignInForm () {
const form = useForm({
onSubmit: (values) => Promise.resolve(true)
})
return (
<Form context={form}>
<Field name="username" />
<Field name="password" />
<Button type="submit">Sign in</Button>
</Form>
)
}
History of releases is in the changelog.
The code is released under the MIT License.