Performant, minimal React form library
- Controlled fields like Formik, but fast (every field has it's own context)
- Excellent Typescript support
Installation
npm install react-nerd
yarn add react-nerd
Usage
import { createForm } from 'react-nerd';
// create the form
const { FormProvider, useField, useFormActions } = createForm({
initialValues: {
firstName: '',
lastName: '',
},
});
// create field components
const FirstName = () => {
const { value, setValue } = useField({ name: 'firstName' });
return (
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
);
};
const LastName = () => {
const { value, setValue } = useField({ name: 'lastName' });
return (
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
);
};
// create form component
const Form = () => {
const { handleSubmit } = useFormActions();
return (
<form onSubmit={handleSubmit}>
<FirstName />
<LastName />
</form>
);
};
// wrap Form with FormProvider so it can call useFormActions
const FormPage = () => {
const onSubmit = values => {
console.log(values);
};
return (
<FormProvider onSubmit={onSubmit}>
<Form />
</FormProvider>
);
};
// And that's it ! 🥳
Validation
This library doesn't support validation schemas out of the box, and it only has field level validation via the validate
function. We believe that is enough.
You should return either a boolean or some object whose leaf nodes are boolean from the validate
function.
When this library calculates isValid
it checks whether any of the leaf nodes in the validation state are false
, and if so, the form is not valid.
So, in general, you shouldn't return error message strings from the validate
function like you would do in Formik,
but rather calculate the error message from the validation result in the render of the component like in the example below.
You can use the nifty library fun-validation
import { isStringLongerThan } from 'fun-validation';
const FirstName = () => {
const { value, setValue, validation } = useField({
name: 'firstName',
// whatever you return from the validate function, you will get back from useField
validate: value => isStringLongerThan(0)(value),
});
const error = validation === false ? 'Required' : undefined;
return (
<div>
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
/>
{error && <div style={{ color: 'red' }}>{error}</div>}
</div>
);
};
useField: ({name: string, validate: (value: any) => any}) => {value, validation, setValue, setBlur}
Imperative actions
Just use the useFormActions
hook and get the functions. This hook never causes a render, because the functions are memoized.
const ResetButton = () => {
const { resetForm } = useFormActions();
return <button onClick={resetForm}>Reset</button>;
};
const TriggerValidationButton = () => {
const { validateField } = useFormActions();
return (
<button onClick={() => validateField({ name: 'firstName' })}>
Trigger validation
</button>
);
};
List of all imperative actions
setFieldValue: ({name: string, value?: any, shouldValidate?: boolean}) => void
setFieldValidation: ({name: string, validation: any}) => void
setBlur: ({ name: string }) => void;
validateField: ({name: string}) => Promise<FieldValidation>
validateAllFields: () => Promise<Validation>
submitForm: () => Promise<any>
resetForm: (newState?: Partial<NewState>) => void
type NewState<Values> = {
values: Partial<Values>;
validation: Partial<Validation>;
isSubmitting: boolean;
submitCount: number;
};
handleSubmit: (e?: any) => void
handleReset: (e?: any) => void
setValues: ({values: Partial<Values>, shouldValidate?: boolean}) => void
setValidation: ({validation: Partial<Validation>}) => void
Accessing field state
If you need the state of another field (value, validation), use the useFieldState
hook returned from createForm
const FirstName = () => {
const { value, setValue } = useField({ name: 'firstName' });
const { value: lastNameValue } = useFieldState('lastName');
useEffect(() => {
// do something with last name value
}, [lastNameValue]);
return (
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
);
};
useFieldState: ({ name: string }) => FieldState;
type FieldState = {
value: any;
validation: any;
};
Accessing form state
There is a separate hook for reading each form state returned by createForm
. This is done so you can choose which form state you want to subscribe to.
const {
useValues,
useValidation,
useIsDirty,
useIsValid,
useIsSubmitting,
useSubmitCount,
} = createForm({
initialValues: {
firstName: '',
lastName: '',
},
});
const FormState = () => {
const values = useValues();
const validation = useValidation();
const isDirty = useIsDirty();
const isValid = useIsValid();
const isSubmitting = useIsSubmitting();
const submitCount = useSubmitCount();
// do something with form state
return <div>{/*
or render something from form state
*/}</div>;
};
List of hooks for accessing form state
useValues: () => Values;
useValidation: () => Validation;
useIsDirty: () => boolean;
useIsValid: () => boolean;
useIsSubmitting: () => boolean;
useSubmitCount: () => number;
Nested fields
This is how you would implement an array of text fields
import { createForm, append, remove } from 'react-nerd';
const { useField } = createForm({
initialValues: {
users: ['Frank', 'James'],
},
});
// the field array component
const Users = () => {
const { value: users, setValue } = useField({ name: 'users' });
const addUser = () => {
setValue(append(users, 'New user'));
};
return (
<div>
<ul>
{users.map((user, index) => (
<UserItem key={index} user={user} index={index} />
))}
</ul>
<button onClick={addUser}>Add user</button>
</div>
);
};
// you should always memoize an item component in a dynamic list
const UserItem = React.memo(({ user, index }) => {
const { setFieldValue } = useFormActions();
// notice how the UserItem component nowhere depends on the
// value of the whole array, and that's why the memoization will work
const updateUser = (user: string) => {
setFieldValue({
name: "users",
setValue: (users) => replace(users, index, user)
})
}
const removeUser = () => {
setFieldValue({
name: 'users',
setValue: users => remove(users, index),
});
};
return (
<li>
<input type="text" value={user} onChange={e => updateUser(e.target.value)}>
<button onClick={removeUser}>Remove user</button>
</li>
);
});
List of exported helpers for field arrays
prepend: <E>(array: E[], newElement: E) => E[]
append: <E>(array: E[], newElement: any) => any[]
remove: <E>(array: E[], index: number) => E[]
replace: <E>(array: E[], index: number, newElement: E) => E[]
insert: <E>(array: E[], index: number, newElement: E) => E[]
swap: <E>(array: E[], indexA: number, indexB: number) => E[]
move: <E>(array: E[], from: number, to: number) => E[]
Usage with Typescript
// All you have to do is when creating the form, define the type of initial values
type MyFormState = {
firstName: string;
lastName: string;
};
const { FormProvider, useField, useFormActions } = createForm({
initialValues: {
firstName: '',
lastName: '',
} as MyFormState,
});
// And that's it, you have intellisense for everything: field keys, types of field values, type of validation result...etc