Managing form state in React can be a bit unwieldy sometimes. There are plenty of great solutions already available that make managing forms state a breeze. However, many of those solutions are opinionated, packed with tons of features that may end up not being used, and/or requires shipping a few extra bytes!
Luckily, the recent introduction of React Hooks, and the ability to write custom ones have enabled new possibilities when it comes sharing state logic. Forms state is no expectation!
react-use-form-state is a small React Hook that attempts to simplify managing form state, using the native form input elements you are familiar with!
To get it started, add react-use-form-state to your project:
npm install --save react-use-form-state
Please note that react-use-form-state requires react@^16.8.0 as a peer dependency.
import { useFormState } from 'react-use-form-state';
export default function SignUpForm() {
const [formState, { text, email, password, radio }] = useFormState();
return (
<form onSubmit={() => console.log(formState)}>
<input {...text('name')} />
<input {...email('email')} required />
<input {...password('password')} required minLength="8" />
<input {...radio('plan', 'free')} />
<input {...radio('plan', 'premium')} />
</form>
);
}From the example above, as the user fills in the form, formState will look something like this:
{
"values": {
"name": "Mary Poppins",
"email": "[email protected]",
"password": "1234",
"plan": "free",
},
"validity": {
"name": true,
"email": true,
"password": false,
"plan": true,
},
"touched": {
"name": true,
"email": true,
"password": true,
"plan": true,
}
}useFormState takes an initial state object with keys matching the names of the inputs.
export default function RentCarForm() {
const [formState, { checkbox, radio, select }] = useFormState({
trip: 'roundtrip',
type: ['sedan', 'suv', 'van'],
});
return (
<form>
<select {...select('trip')}>
<option value="roundtrip">Same Drop-off</option>
<option value="oneway">Different Drop-off</option>
</select>
<input {...checkbox('type', 'sedan')} />
<input {...checkbox('type', 'suv')} />
<input {...checkbox('type', 'van')} />
<button>Submit</button>
</form>
);
}useFormState supports a variety of form-level event handlers that you could use to perform certain actions:
export default function RentCarForm() {
const [formState, { email, password }] = useFormState(null, {
onChange(e, stateValues, nextStateValues) {
const { name, value } = e.target;
console.log(`the ${name} input has changed!`);
},
});
return (
<>
<input {...text('username')} />
<input {...password('password')} />
</>
);
}useFormState provides a quick and simple API to get started with building a from and managing its state. It also supports HTML5 from validation out of the box.
<input {...password('password')} required minLength="8" />While this covers that majority of validation cases, there are times when you need to attach custom event handlers or perform custom validation.
For this, all input functions provide an alternate API that allows you attach input-level event handlers such as onChange and onBlur, as well as providing custom validation logic.
export default function SignUpForm() {
const [state, { text, password }] = useFormState();
return (
<>
<input {...text('username')} required />
<input
{...password({
name: 'password',
onChange: e => console.log('password input changed!'),
onBlur: e => console.log('password input lost focus!'),
validateOnBlur: true,
validate: (value, values) =>
!value.includes(values.username) &&
STRONG_PASSWORD_REGEX.test(value),
})}
/>
</>
);
};useFormState is not limited to actual forms. It can be used anywhere inputs are used.
function LoginForm({ onSubmit }) {
const [formState, { email, password }] = useFormState();
return (
<div>
<input {...email('email')} required />
<input {...password('password')} required minLength="8" />
<button onClick={() => onSubmit(formState)}>Login</button>
</div>
);
}As a convenience, useFormState provides an optional API that helps with pairing a label to a specific input.
When formOptions.withIds is enabled, a label can be paired to an input by using input.label(). This will populate the label's htmlFor attribute for an input with the same parameters.
const [formState, { label, text, radio }] = useFormState(initialState, {
withIds: true, // enable automatic creation of id and htmlFor props
});
return (
<form>
<label {...label('name')}>Full Name</label>
<input {...text('name')} />
<label {...label('plan', 'free')}>Free Plan</label>
<input {...radio('plan', 'free')} />
<label {...label('plan', 'premium')}>Premium Plan</label>
<input {...radio('plan', 'premium')} />
</form>
);Note that this will override any existing id prop if specified before calling the input functions. If you want the id to take precedence, it must be passed after calling the input types like this:
<input {...text('username')} id="signup-username" />When working with TypeScript, the compiler needs to know what values and inputs useFormState is expected to be working with.
For this reason, useFormState accepts an optional type argument that defines the state of the form and its fields which you could use to enforce type safety.
interface LoginFormFields {
username: string;
password: string;
remember_me: boolean;
}
const [formState, { text }] = useFormState<LoginFormFields>();
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
// OK
<input {...text('username')} />
formState.values.username
// Error
formState.values.doesNotExist
<input {...text('doesNotExist')} />By default, useFormState will use the type any for the form state and its inputs if no type argument is provided. Therefore, it is recommended that you provide one.
import { useFormState } from 'react-use-form-state';
function FormComponent()
const [formState, inputs] = useFormState(initialState, formOptions);
// ...
}useFormState takes an optional initial state object with keys as the name property of the form inputs, and values as the initial values of those inputs (similar to defaultValue/defaultChecked).
useFormState also accepts an optional form options object as a second argument with following properties:
A function that gets called upon any blur of the form's inputs. This functions provides access to the input's blur SyntheticEvent
const [formState, inputs] = useFormState(null, {
onBlur(e) {
// accessing the inputs target that triggered the blur event
const { name, value, ...target } = e.target;
}
});A function that gets triggered upon any change of the form's inputs, and before updating formState.
This function gives you access to the input's change SyntheticEvent, the current formState, the next state after the change is applied.
const [formState, inputs] = useFormState(null, {
onChange(e, stateValues, nextStateValues) {
// accessing the actual inputs target that triggered the change event
const { name, value, ...target } = e.target;
// the state values prior to applying the change
formState.values === stateValues; // true
// the state values after applying the change
nextStateValues;
// the state value of the input. See Input Types below for more information.
nextStateValues[name];
}
});A function that gets called after an input inside the form has lost focus, and marked as touched. It will be called once throughout the component life cycle. This functions provides access to the input's blur SyntheticEvent.
const [formState, inputs] = useFormState(null, {
onTouched(e) {
// accessing the inputs target that triggered the blur event
const { name, value, ...target } = e.target;
}
});Indicates whether useFormState should generate and pass an id attribute to its fields. This is helpful when working with labels.
It can be one of the following:
A boolean indicating whether input types should pass an id attribute to the inputs (set to false by default).
const [formState, inputs] = useFormState(null, {
withIds: true,
});Or a custom id formatter: a function that gets called with the input's name and own value, and expected to return a unique string (using these parameters) that will be as the input id.
const [formState, inputs] = useFormState(null, {
withIds: (name, ownValue) =>
ownValue ? `MyForm-${name}-${ownValue}` : `MyForm-${name}`,
});Note that when withIds is set to false, applying input.label() will be a no-op.
The return value of useFormState. An array of two items, the first is the form state, and the second an input types object.
The first item returned by useFormState.
const [formState, inputs] = useFormState();An object describing the form state that updates during subsequent re-renders.
Form state consists of three nested objects:
values: an object holding the state of each input being rendered.validity: an object indicating whether the value of each input is valid.touched: an object indicating whether the input was touched (focused) by the user.
formState = {
values: {
[inputName: string]: string | string[] | boolean,
},
validity: {
[inputName: string]: boolean,
},
touched: {
[inputName: string]: boolean,
},
}The second item returned by useFormState.
const [formState, inputs] = useFormState();An object with keys as input types. Each type is a function that returns the appropriate props that can be spread on the corresponding input.
The following types are currently supported:
| Type and Usage | State Shape |
|---|---|
<input {...input.email(name: string) /> |
{ [name: string]: string } |
<input {...input.color(name: string) /> |
{ [name: string]: string } |
<input {...input.password(name: string) /> |
{ [name: string]: string } |
<input {...input.text(name: string) /> |
{ [name: string]: string } |
<input {...input.url(name: string) /> |
{ [name: string]: string } |
<input {...input.search(name: string) /> |
{ [name: string]: string } |
<input {...input.number(name: string) /> |
{ [name: string]: string } |
<input {...input.range(name: string) /> |
{ [name: string]: string } |
<input {...input.tel(name: string) /> |
{ [name: string]: string } |
<input {...input.radio(name: string, ownValue: string) /> |
{ [name: string]: string } |
<input {...input.checkbox(name: string, ownValue: string) /> |
{ [name: string]: Array<string> } |
<input {...input.checkbox(name: string) /> |
{ [name: string]: boolean } |
<input {...input.date(name: string) /> |
{ [name: string]: string } |
<input {...input.month(name: string) /> |
{ [name: string]: string } |
<input {...input.week(name: string) /> |
{ [name: string]: string } |
<input {...input.time(name: string) /> |
{ [name: string]: string } |
<select {...input.select(name: string) /> |
{ [name: string]: string } |
<select {...input.selectMultiple(name: string) /> |
{ [name: string]: Array<string> } |
<textarea {...input.textarea(name: string) /> |
{ [name: string]: string } |
<label {...input.label(name: string, value?: string)} /> |
N/A – input.label() is stateless and thus does not affect the form state |
Alternatively, input type functions can be called with an object as the first argument. This object is used to extend the functionality of the input. This includes attaching event handlers and performing input-level custom validation.
<input
{...text({
name: 'username',
validate: value => validateUsername(value),
validateOnBlur: true,
})}
/>The following options can be passed:
| key | Description |
|---|---|
name: string |
Required. The name of the input. |
value: string |
The input's own value. Only required by the radio input, and optional for the checkbox input. |
onChange(e): void |
Optional. A change event handler that gets passed the input's change SyntheticEvent. |
onBlur(e): void |
Optional. A blur event handler that gets passed the input's blur SyntheticEvent. |
validate(value: string, values: StateValues): boolean |
Optional. An input validation function that gets passed the input value and all input values in the state. It's expected to return a boolean indicating whether the input's value is valid. HTML5 validation rules are ignored when this function is specified. |
validateOnBlur: boolean |
Optional. false by default. When set to true and the validate function is provided, the function will be called when the input loses focus. If not specified, the validate function will be called on value change. |
MIT