react-bulk-form
Version:
A simple React library for managing form-related states in bulk.
134 lines (117 loc) • 3.78 kB
text/typescript
import { useState, useMemo, useEffect, useCallback } from 'react';
import { isEqual } from '../utils/isEqual';
import type {
FormValues,
FormRuleKey,
UseFormOptions,
UseFormReturn,
FormErrors,
FormRules
} from '../types/common';
export function useForm<V extends FormValues<V>, R extends FormRuleKey = never>(
options: UseFormOptions<V, R>
): UseFormReturn<V, R> {
const [values, setValues] = useState<V>(options.defaultValues);
const [errors, setErrors] = useState<FormErrors<R>>({});
const [defaultValues, setDefaultValues] = useState<V>(options.defaultValues);
const [rules] = useState<FormRules<V, R> | undefined>(
(options as { rules: FormRules<V, R> | undefined }).rules
);
const dirtyFields = useMemo(
() =>
Object.keys(values).reduce(
(acc, _key) => {
const key = _key as keyof V;
if (!isEqual(values[key], defaultValues[key])) {
acc[key] = true;
}
return acc;
},
{} as { [P in keyof V]?: true }
),
[values, defaultValues]
);
const [touchedFields, setTouchedFields] = useState<{ [P in keyof V]?: true }>({});
const isValid = useMemo(() => Object.keys(errors).length === 0, [errors]);
const isDirty = useMemo(() => Object.keys(dirtyFields).length > 0, [dirtyFields]);
// Validate the form values when the values or the rules change.
useEffect(() => {
const newErrors: Record<string, boolean> = {};
if (rules) {
Object.keys(rules).forEach((_ruleKey) => {
const ruleKey = _ruleKey as R;
const rule = rules[ruleKey];
if (!rule(values)) {
newErrors[ruleKey] = true;
}
});
}
setErrors(newErrors as FormErrors<R>);
}, [values, rules]);
const setPartialValues = useCallback(
(valuesOrCallback: Partial<V> | ((prevValues: V) => Partial<V>)) => {
setValues((prevValues) => {
const newPartialValues = Object.entries(
typeof valuesOrCallback === 'function' ? valuesOrCallback(prevValues) : valuesOrCallback
).reduce((acc, [_key, _value]) => {
const key = _key as keyof V;
const value = _value as V[typeof key];
if (value !== undefined) {
acc[key] = value;
setTouchedFields((prevTouchedFields) => ({ ...prevTouchedFields, [key]: true }));
}
return acc;
}, {} as Partial<V>);
return { ...prevValues, ...newPartialValues };
});
},
[]
);
const setPartialErrors = useCallback(
(
errorsOrCallback:
| Record<string, boolean>
| ((prevErrors: FormErrors<R>) => Record<string, boolean>)
) => {
setErrors((prevErrors) => {
const newPartialErrors = Object.entries(
typeof errorsOrCallback === 'function' ? errorsOrCallback(prevErrors) : errorsOrCallback
).reduce(
(acc, [key, _value]) => {
const value = _value as boolean;
if (value) {
acc[key] = true;
} else {
delete acc[key];
}
return acc;
},
{} as Record<string, boolean>
);
return { ...prevErrors, ...newPartialErrors };
});
},
[]
);
const reset = useCallback(() => {
setValues(defaultValues);
setErrors({});
}, [defaultValues]);
const commit = useCallback(() => {
setDefaultValues(values);
}, [values]);
return useMemo(
() => ({
values,
errors,
dirtyFields,
touchedFields,
flags: { isValid, isDirty },
setValues: setPartialValues,
setErrors: setPartialErrors,
reset,
commit
}),
[values, errors, isValid, isDirty, setPartialValues, setPartialErrors, reset, commit]
);
}