3 ways to build forms in react (without any libraries)
Using a library when building complex forms is likely still very helpful - react-hook-form with zod is a robust solution used in production in many applications today. But if you just want to build a quick form, do you really need a library?
I used to jump to building a quick form with controlled inputs in the past, but lately I realised going for a default browser form with uncontrolled inputs works even nicer!
Here is an overview of three approaches for building forms in React without any 3rd party libraries by building a "user feedback" form.
<FeedbackForm
feedback={itemBeingEdited || undefined}
onSave={handleSave}
onCancel={() => setItemBeingEdited(null)}
/>
1 - Form with uncontrolled inputs
This is my current preferred way, as it just uses native browser features and the code is very readable:
- form fields need to have the
name
attribute specified so we can get the values withFormData
(docs) - we can clear the values with
form.reset()
(docs)
import { FormEvent, MouseEvent } from "react";
import { UserFeedback, UserFeedbackFormFields } from "../types";
type Props = {
// optional prop - initial value in case we are editing an item
feedback?: UserFeedback;
onSave: (newValues: UserFeedbackFormFields | UserFeedback) => void;
onCancel: () => void;
};
const UncontrolledForm: React.FC<Props> = ({ feedback, onSave, onCancel }) => {
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const data = new FormData(form);
onSave({
...feedback,
name: data.get("name") as string,
suggestion: data.get("suggestion") as string,
});
form.reset();
};
const handleReset = (e: MouseEvent<HTMLButtonElement>) => {
const form = (e.target as HTMLButtonElement).form as HTMLFormElement;
form.reset();
onCancel();
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" name="name" defaultValue={feedback?.name} />
</label>
<label>
Suggestion:
<input
type="text"
name="suggestion"
defaultValue={feedback?.suggestion}
/>
</label>
<div>
<button>Save</button>
<button type="button" onClick={handleReset}>
Cancel
</button>
</div>
</form>
);
};
export default UncontrolledForm;
2 - Form with controlled inputs
This is the approach you will probably find most often in tutorials out there.
It used to be the go to for forms and it's still useful if you need the values in places other than the submit handler itself.
import { FormEvent, useState } from "react";
import { UserFeedback, UserFeedbackFormFields } from "../types";
type Props = {
// optional prop - initial value in case we are editing an item
feedback?: UserFeedback;
onSave: (newValues: UserFeedbackFormFields | UserFeedback) => void;
onCancel: () => void;
};
const ControlledForm: React.FC<Props> = ({ feedback, onSave, onCancel }) => {
const [name, setName] = useState<string>(feedback?.name || "");
const [suggestion, setSuggestion] = useState<string>(
feedback?.suggestion || ""
);
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSave({
...feedback,
name,
suggestion,
});
setName("");
setSuggestion("");
};
const handleReset = () => {
setName("");
setSuggestion("");
onCancel();
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<label>
Suggestion:
<input
type="text"
value={suggestion}
onChange={(e) => setSuggestion(e.target.value)}
/>
</label>
<div>
<button>Save</button>
<button type="button" onClick={handleReset}>
Cancel
</button>
</div>
</form>
);
};
export default ControlledForm;
3 - Form with React 19 action attribute
This is the new approach introduced with React 19 - using the action
attribute on the form element, together with the useActionState hook. For simple forms, I find this to be too verbose, but it shines when doing async server actions, or even better, when used with SSR.
It's nice that the fields get reset for you on submit (though sometimes you might not want that, like when there's an error 😅).
import { useActionState, MouseEvent } from "react";
import { UserFeedback, UserFeedbackFormFields } from "../types";
type Props = {
// optional prop - initial value in case we are editing an item
feedback?: UserFeedback;
onSave: (newValues: UserFeedbackFormFields | UserFeedback) => void;
onCancel: () => void;
};
const React19Form: React.FC<Props> = ({ feedback, onSave, onCancel }) => {
const [_, submitAction] = useActionState(
async (_: UserFeedbackFormFields, formData: FormData) => {
onSave({
...feedback,
name: formData.get("name") as string,
suggestion: formData.get("suggestion") as string,
});
return "form saved successfully";
},
null
);
const handleReset = (e: MouseEvent<HTMLButtonElement>) => {
const form = (e.target as HTMLButtonElement).form as HTMLFormElement;
form.reset();
onCancel();
};
return (
<form action={submitAction}>
<label>
Name:
<input type="text" name="name" defaultValue={feedback?.name} />
</label>
<label>
Suggestion:
<input
type="text"
name="suggestion"
defaultValue={feedback?.suggestion}
/>
</label>
<div>
<button>Save</button>
<button type="button" onClick={handleReset}>
Cancel
</button>
</div>
</form>
);
};
export default React19Form;
You can checkout the full working code for the three forms above on CodeSandbox: https://codesandbox.io/p/sandbox/three-ways-to-build-forms-2s9qpd.
How do you build simple forms with React?
Do you prefer any version over the other? Why?
Member discussion