Let's be honest – most developers aren't big fans of building forms. They can be a bit of a headache! But here's the thing, forms are actually super important in web development. You see them everywhere when you go online. I mean, even Google's main page is basically just a search form!

Sure, there are tons of React packages out there that promise to make forms easier. But honestly? You probably don't need them. Working with forms in React is simpler than you might think.
In this guide, I'll show you how to connect your form inputs with your data, explain a super critical React concept, and share some tricks to make building bigger forms way less painful.
Data binding
When building web applications, we often want to sync a bit of state to a particular form input. For example, a "username" field should be bound to the value of a username
state variable.
This is commonly known as data binding. Most front-end frameworks offer a way to bind a particular bit of state to a particular form control.
Let me show you how this works in React:
Here are some explanations about the code sandbox above:
- The
value
attribute works differently in React than it does in HTML.
→ In HTML, value sets the default value, and can be edited
→ In React, value locks the input to the specified value, and becomes read-only - By setting
value
equal to oursearchWord
state variable, we ensure that the input will always display the search term. - Essentially, our data binding is 50% complete. The input will always show the value of the
searchWord
state, even when that value is updated, but the state can't be changed by the input. The binding is only one way. - When we add
onChange
handler, we see that the input actually does briefly update to show the new value. The problem is that React will undo that change immediately after the change event fires, before the browser has even had the time to complete a single repaint. - We can call
setSearchWord
with the input’s current value as a way to persist that edit. When React re-renders, the input will be updated to show the updated value held in state.
React uses a synthetic event system. These events are special objects created by React, not the standard events used in JavaScript.
Why does React do this? Several reasons:
→ It can ensure consistency, removing some edge-case issues with native events being implemented slightly differently between browsers.
→ It can include a few helpful “extras”, to improve the developer experience.
If you ever need to access the “real” event object, you can access it like this:
<input onChange={(event) => {
const realEvent = event.nativeEvent;
console.log(realEvent); // DOM InputEvent object
}}
/>
Controlled vs uncontrolled inputs
Here's something cool about React forms - when you add a value
attribute to an input, you're telling React, "Hey, I want you to take charge of this input." We call this a controlled input because React is the boss here!
On the flip side, if you don't set a value
, it's called an uncontrolled input - React just lets it do its own thing.
Here's a super important tip: An input should always either be controlled or uncontrolled. Your input should either be controlled or uncontrolled. React gets confused if you try to switch between the two.
This is actually a common mistake that can cause headaches. Let me show you what I mean.
Consider this solution:
If you try to type in the text input, and after that go to the Console view. You will see a warning that begins like this:

You might be thinking, "Wait, what? This input is controlled! We're clearly setting value={username}
right from the start!"
Here's what's happening: When we first render, username
is undefined because we haven't set an initial value in our state hook. Let me show you a simpler version:
const username = undefined;
<input
type="text"
id="username"
value={username}
onChange={event => {
setUsername(event.target.value);
}}
/>
When we set value
to undefined
, it's the same as not setting it at all. React will treat the input as an uncontrolled input.
When the user starts typing in the input, the onChange
event updates the value of username
from undefined
to a string. And so, React flips the element from and uncontrolled input to a controlled input, and raises the warning.
Here's how to solve the problem: We always want to make sure we're passing a defined value
. We can do this by initializing username
to an empty string:
// 🚫 Incorrect. `username` will flip from `undefined` to a string:
const [username, setUsername] = React.useState();
// ✅ Correct. `username` will always be a string:
const [username, setUsername] = React.useState('');
With this change, our input is being controlled by React state from the very first render, since we're always passing a defined value. Even though empty strings are considered falsy, they still “count” when it comes to controlling React inputs.
The onClick misunderstanding
Let suppose we want to build a search form, we have 2 files:
- App.js
- SearchForm.js
Now let say we the following code in our App.js
:
import SearchForm from './SearchForm';
function App() {
// This function is a placeholder.
function runSearch(searchTerm) {
window.alert(`Searched for: ${searchTerm}`);
}
return (
<SearchForm runSearch={runSearch} />
);
}
export default App;
And the following code in our SearchForm.js
:
import React from 'react';
function SearchForm({ runSearch }) {
const [searchTerm, setSearchTerm] = React.useState('');
return (
<div className="search-form">
<input
type="text"
value={searchTerm}
onChange={event => {
setSearchTerm(event.target.value);
}}
/>
<button>
Search!
</button>
</div>
);
}
export default SearchForm;
In this example, above runSearch
is the function we would like to call when the user clicks/taps the “Search!” button.
Here is the question: How should I use this function?
Plenty of developers instinctively solve for this by adding an onClick handler to the submit button:
<button onClick={() => runSearch(searchTerm)}>
Search!
</button>
There are several problems with this approach. For example, what if the user tries to search by pressing “Enter” after typing in the text input?
Well, you might think we could tackle that with an onKeyDown
event listener
<input
type="text"
value={searchTerm}
onChange={event => {
setSearchTerm(event.target.value);
}}
onKeyDown={event => {
if (event.key === 'Enter') {
runSearch(searchTerm);
}
}}
/>
This is awful practice because we're doing by hand what the browser is supposed to do for us because he already knows how to do.
Use a form
To solve this problem, we should wrap our form controls in a <form>
tag. Then, instead of listening for clicks and keys, we can listen for the form submit event.
Look how much simpler the code gets:
The form submits event will be called automatically when the user clicks the button, or presses "Enter" whenever the input or button is focused. When that event fires, we'll run our search.
Instead of trying to re-create a bunch of standard web platform stuff, we should use the platform and let it solve these sorts of problems for us!
By using a form submit event, we get to use client-side validation:
<input
type="password"
required={true}
minLength={8}
/>
Learn more about validation attributes like required
, minLength
, and pattern
on MDN (opens in new tab).
Default form behaviour
Alright, so there is one little quirk* with using onSubmit
. We need to prevent the default submission behaviour:
<form
className="search-form"
onSubmit={event => {
event.preventDefault();
runSearch(searchTerm);
}}
>
To understand why this is necessary, let's look back to the early days of the web—before fetch
, before XMLHttpRequest
, before JSON.
Back then, when you needed to fetch search results from a server, you couldn't just request the data alone. Instead, you had to request an entirely new HTML file. The server would take your form data, create a new HTML document from a template, and redirect you to a new URL to display the results.
Forms still operate this way by default. When you submit a form, the browser attempts to send the user to the URL specified by the action
attribute:
<!--
Submitting this form will redirect the user to the
/search page, sending along the data collected from
the form fields.
-->
<form
method="POST"
action="/search"
>
If you don't add an action
attribute to your form, the browser will just reload the current page when you submit it.
But here's the thing - in modern React apps, we usually don't want that. Instead of refreshing the whole page, we just want to grab some data and update parts of our page with it. This makes everything feel faster and smoother for users.
That's precisely why we use event.preventDefault()
. It tells the browser, "Hey, don't reload the page!" when someone submits the form.
Other forms control
Hey, besides regular text inputs, there are actually lots of other cool form controls you can use in your web apps. Here's what you've got to play with:
- Big text boxes [Textareas]
- Pick-one buttons [Radio buttons]
- Tick boxes [Checkboxes]
- Dropdown menus [Selects]
- Sliders [Ranges]
- Color wheels [Color pickers]
Now, if you've ever tried working with these outside of React, you probably know they can be a bit tricky - each one works differently, which can be pretty confusing!
For example, textareas define the current value as children, rather than using a value
prop:
<textarea>
This is the current value
</textarea>
As another example, select tags use a selected
prop on one of the <option>
children to signify the selected value:
<select>
<option value="a">
Option 1
</option>
<option value="b" selected>
Option 2
</option>
<option value="c">
Option 3
</option>
</select>
Here's the good news: React has tweaked many of these form controls so that they all behave much more similarly. There's a lot less chaos with form controls in React.
Essentially, all form controls follow the same pattern:
- The current value is locked using either
value
(for most inputs) orchecked
(for checkboxes and radio buttons). - We respond to changes with the
onChange
event listener.
In the “Data Binding” Section, we learned about “controlled inputs”. The word input in this case refers not only to the
<input>
HTML element, but to any form control, including <textarea>
, <select>
, etc.In fact, when I talk about “inputs”, you can generally assume I mean any form element that accepts user input. If I'm referring specifically to the
<input>
tag, I'll say “text input”.Select tag
When working with select tags in React, they work almost exactly like text inputs. We use value
and onChange
. Here's an example:
By setting the value
prop, we make this a controlled component, binding the selected option to our React state. When we add the onChange
event listener, we allow this state to be changed by selecting a different option from the list.
This is so much better than how forms used to work! We don't need to mess around with that trickyselected
attribute any more for our dropdown options. Instead, we can just use the same simple pattern we know and love.
Radio buttons
Alright, so radio buttons are a bit trickier.
Ostensibly, they serve the same purpose as a <select>
tag; they allow the user to select 1 choice from a group of options.
The tricky thing, though, is that this state is split across multiple independent HTML elements. There's only one <select>
tag, but there are multiple <input type="radio">
buttons!
Let's start by looking at an example of a controlled radio button group in React:
Let's break down these important properties one by one.
name
: Radio buttons in the same group must share the samename
prop. This tells the browser they're connected, ensuring that selecting one option automatically deselects the others.value
: Each radio button needs its own unique value. When an option is selected, this value gets stored in our React state.id
: This connects each radio button to its<label>
, allowing users to select an option by clicking its label.checked
: This boolean prop connects the radio button to our React state. Set it totrue
for the selected option andfalse
for all others.
While this might look different from other form controls, it follows the same basic pattern. We listen for changes with onChange
and update our React state when a selection is made.
Unlike most form inputs that use the value
prop for state binding, radio buttons are special. Since we're dealing with multiple buttons, we use the checked
prop instead to control which option is selected.
Both radio buttons and
<select>
tags serve the same purpose: letting users choose one option from a list. But when should you use each one?Here are some practical guidelines:
→ Use a
<select>
for lengthy option lists (like countries) to keep your interface clean and prevent overwhelming users with choices.→ Choose radio buttons when users need to carefully consider each option (like privacy settings). Users tend to read and consider radio button options more thoroughly.
→ If you have a recommended default choice, use a
<select>
to help users move through your form more efficiently.Wrap up
Before you wrap up your React forms, consider using libraries such as Formik. These tools can simplify complex forms and manage state efficiently. Although I don’t recommend using libraries for forms validation, it can be pretty useful to use tools like that to improve your speed, if you already know how to handle forms in a React way. They offer built-in validation and help maintain clean code, making your life a lot easier!
🔍. Similar posts
How to Effectively Use the React useId Hook in Your Apps
16 Feb 2025
The Simple Method for Lifting State Up in React Effectively
16 Feb 2025
How to Master Dynamic Key Generation in React for Your Apps
12 Feb 2025