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 ofisDirty
- 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)
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:
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>
);
}
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:
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:
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>
);
}
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:
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>
);
}
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.
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:
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>
);
}
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:
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>
);
}
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:
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>
);
}
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.