Skip to main content

Form states

@per-form/react states values

@per-form/react expose a states object that contains form states:

  • changedFields (string[]): List of fields the user has changed
  • dirtyFields (string[]): List of fields the user has changed and values are different to default ones
  • isDirty (boolean): Whether the form is dirty or not (at least one field is dirty)
  • isPristine (boolean): Inverse of isDirty
  • isReady (boolean): Indicates when the form is ready
  • isSubmitted (boolean): true when the form has been submitted at least once
  • isSubmitting (boolean): Indicates when the form is being submitted
  • isValid (boolean): Whether the form is valid or not
  • isValidating (boolean): Indicates when the form is being validated
  • submitCount (number): Count the number of time the form has been submitted
  • touchedFields (string[]): List of fields the user has interacted with (focus + blur)
warning

The states object is not a React state so you should not use it directly in your renders !

This example will not work has expected:

0
import type { FormEvent } from 'react';
import type { IProps } from '../types';
import { type IFormValues, useForm } from '@per-form/react';

export default function Demo(props: IProps) {
function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}

const { errors, formProps, states } = useForm({
...props,
onSubmit: handleSubmit,
});

return (
<form {...formProps}>
<input name="text" required />
{errors.all.text && <div className="error">{errors.all.text}</div>}
<button disabled={!states.isValid} type="submit">
Submit
</button>
</form>
);
}
info

The button stay disabled even when you fill the input because by default @per-form/react do not trigger unwanted renders.

See below on how to retrieve the form state correctly.

With the Submit button

The previous example, where the <submit> button is disabled while the form is invalid can be created by using the <Submit> component with the disableOnError props:

0
import type { FormEvent } from 'react';
import type { IProps } from '../types';
import {
FormProvider,
type IFormValues,
Submit,
useForm,
} from '@per-form/react';

export default function Demo(props: IProps) {
function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}

const { formProps, ...context } = useForm({
...props,
onSubmit: handleSubmit,
});
const { errors } = context;

return (
<FormProvider {...context}>
<form {...formProps}>
<input name="text" required />
{errors.all.text && <div className="error">{errors.all.text}</div>}
<Submit disableOnError />
</form>
</FormProvider>
);
}

With the useFormValid hook

More generic than the previous case but still dedicated to the valid state, you can use the useFormValid hook to get the form valid state as a real React state:

0
import type { FormEvent } from 'react';
import type { IFormValues } from '@per-form/react';
import type { IProps } from '../types';
import { FormProvider, useForm, useFormValid } from '@per-form/react';

function Submit() {
const isValid = useFormValid();
return (
<button disabled={!isValid} type="submit">
Submit
</button>
);
}

export default function Demo(props: IProps) {
function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}

const { formProps, ...context } = useForm({
...props,
onSubmit: handleSubmit,
});
const { errors } = context;

return (
<FormProvider {...context}>
<form {...formProps}>
<input name="text" required />
{errors.all.text && <div className="error">{errors.all.text}</div>}
<Submit />
</form>
</FormProvider>
);
}
warning

With the hook version you will have to add yourself the @per-form/react <FormProvider> component !

With the useFormStates hook

The useFormStates hook will return you all form state as a real React state:

0
{
  "changedFields": [],
  "isSubmitting": false,
  "isValid": true,
  "isValidating": false,
  "submitCount": 0,
  "touchedFields": [],
  "isReady": false,
  "dirtyFields": [],
  "isChanged": false,
  "isDirty": false,
  "isPristine": true,
  "isSubmitted": false,
  "isTouched": false
}
import type { FormEvent } from 'react';
import type { IFormValues } from '@per-form/react';
import type { IProps } from '../types';
import { FormProvider, useForm, useFormStates } from '@per-form/react';
import { delay } from '../time';

const defaultValues = { a: 'foo' };
const validator = (value: string) => (values: IFormValues, names: string[]) =>
delay(
String(values[names[0]]).includes(value)
? ''
: `Value does not include "${value}"`,
);
const validators = {
a: validator('foo'),
b: validator('bar'),
c: validator('baz'),
};

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(props: IProps) {
function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
return delay();
}

const { formProps, ...context } = useForm({
...props,
defaultValues,
onSubmit: handleSubmit,
validators,
});
const { errors } = context;

return (
<FormProvider {...context}>
<form {...formProps}>
<input name="a" required />
{errors.all.a && <div className="error">{errors.all.a}</div>}
<input defaultValue="bar" name="b" required />
{errors.all.b && <div className="error">{errors.all.b}</div>}
<input name="c" required />
{errors.all.c && <div className="error">{errors.all.c}</div>}
<Submit />
</form>
</FormProvider>
);
}
info

isDirty, isPristine and dirtyFields compare field values to defaultValues (a field) or defaultValue (b field) or to empty string "" (c field) to define their states.

note

You can pass a field name to useFormStates to get the field state instead of the whole form state.

With the useSubscribe hook

You can use the useSubscribe hook to listen to state changes.

You can then provide a callback that will for example update a state:

0
import { type FormEvent, useState } from 'react';
import type { IProps } from '../types';
import {
FormProvider,
type IFormValues,
useForm,
useSubscribe,
} from '@per-form/react';

function Submit() {
const [isValid, setIsValid] = useState(false);
useSubscribe(({ isValid }) => setIsValid(isValid));

return (
<button disabled={!isValid} type="submit">
Submit
</button>
);
}

export default function Demo(props: IProps) {
function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}

const { formProps, ...context } = useForm({
...props,
onSubmit: handleSubmit,
});
const { errors } = context;

return (
<FormProvider {...context}>
<form {...formProps}>
<input name="text" required />
{errors.all.text && <div className="error">{errors.all.text}</div>}
<Submit />
</form>
</FormProvider>
);
}
warning

Again, with the hook version you will have to add yourself the provider.

With the subscribe function

You can subscribe to state changes with the subscribe function.

In that case you need to declare both the useState and the useEffect yourself:

0
import { type FormEvent, useEffect, useState } from 'react';
import type { IProps } from '../types';
import { type IFormValues, useForm } from '@per-form/react';

export default function Demo(props: IProps) {
function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}

const { errors, formProps, states, subscribe } = useForm({
...props,
onSubmit: handleSubmit,
});

const [isValid, setIsValid] = useState(states.isValid);
useEffect(() => {
return subscribe(({ isValid }) => setIsValid(isValid));
}, [subscribe]);

return (
<form {...formProps}>
<input name="text" required />
{errors.all.text && <div className="error">{errors.all.text}</div>}
<button disabled={!isValid} type="submit">
Submit
</button>
</form>
);
}
info

The subscribe function return a function for unsubscribing. You can directly return that function in your useEffect.

With disabled inputs

Form validation is working as expected even with dynamic disabled inputs:

0
import { type FormEvent, useState } from 'react';
import type { IFormValues } from '@per-form/react';
import type { IProps } from '../types';
import { FormProvider, useForm, useFormValid } from '@per-form/react';

function Submit() {
const isValid = useFormValid();
return (
<button disabled={!isValid} type="submit">
Submit
</button>
);
}

export default function Demo(props: IProps) {
const [disabled, setDisabled] = useState(false);

function handleToggle() {
setDisabled((x) => !x);
}

function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}

const { formProps, ...context } = useForm({
...props,
onSubmit: handleSubmit,
});
const { errors } = context;

return (
<FormProvider {...context}>
<form {...formProps}>
<div className="flex">
<input disabled={disabled} name="text" required />
<button onClick={handleToggle} type="button">
{disabled ? 'Enable' : 'Disable'}
</button>
</div>
{errors.all.text && <div className="error">{errors.all.text}</div>}
<Submit />
</form>
</FormProvider>
);
}
info

The submit button is disabled when the input field is empty.

But the submit button is then enabled when the field is disabled even if the field is still empty.