DEV Community

Rakesh Daniel
Rakesh Daniel

Posted on

React useState Basics: Build Dynamic UI with Vite

React apps look like normal HTML pages, but they behave differently. When a user clicks a button, types in an input, selects a checkbox, or changes a dropdown, the UI should update immediately.

That is why we use state in React.

useState stores changing data inside a component. When we call its setter function—for example setCount, setUser, or setForm—React saves the next value, renders the component again, and updates only the required UI parts. State updates are applied for the next render, so logging a state variable immediately after calling its setter may still show the previous value.
What are React and Vite?
React is a JavaScript library for building user interfaces. Instead of manually finding HTML elements and changing them with DOM code, we describe how the UI should look for the current data. When state changes, React updates the screen to match that new state.

Vite is the tool used to create and run the React project quickly. It provides a fast development server and Hot Module Replacement (HMR), so changes in your code appear in the browser very quickly during development. It also creates an optimized production build.

For example, a Vite React project can start with:

npm create vite@latest my-react-app
cd my-react-app
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

**
Virtual DOM in Simple Words**
The browser has a real DOM—the actual HTML elements displayed on the page:
<h1>count: 0</h1>

When the count becomes 1, React does not need to reload the complete page manually.

React creates a lightweight JavaScript representation of the UI, often called the Virtual DOM. When state changes:

React creates the next UI representation.

React compares the previous UI with the new UI.

React finds what changed.

React updates the necessary part of the real browser DOM.

For example, in this code:
<h1>count: {count}</h1>

If count changes from 0 to 1, React updates the displayed count. Your header, buttons, form, and other components do not need a full page refresh.

This is why React feels dynamic and fast.

Why useState Is Important

The syntax is:

const [value, setValue] = useState(initialValue);

Example:
const [count, setCount] = useState(0);
Here:

  • count is the current state value.
  • setCount updates the value.
  • 0 is the initial value.

When a button calls:
setCount(count + 1);
React stores the next count value and re-renders the component with the updated value. React’s official documentation describes useState as a Hook that adds a state variable to a component; its setter updates the state and triggers a re-render.

Your counter example:

const [count, setCount] = useState(0);

<button onClick={() => setCount(count + 1)}>
  Increment
</button>

<button onClick={() => setCount(count - 1)}>
  Decrement
</button>

<button onClick={() => setCount(0)}>
  Reset
</button>
Enter fullscreen mode Exit fullscreen mode

Real-world uses of useState

We use useState whenever data can change because of user actions or application events:

  • Login form values: email, password, OTP.
  • Like button: liked / not liked.
  • Shopping cart quantity.
  • Search input value.
  • Dark mode / light mode.
  • Modal open / close.
  • Loading spinner: loading / completed.
  • Selected language, country, state, city.
  • Todo list items.
  • API response data.
  • Error messages and success messages. A normal JavaScript variable can store a value, but it does not tell React to render again. State does.

1. Toggle Button: ON and OFF

const [isOn, setIsOn] = useState(false);
A boolean has only two values:
true
false
That perfectly matches a two-state feature:

UI meaning Boolean value
OFF false
ON true

Example:

import { useState } from "react";

function ToggleButton() {
  const [isOn, setIsOn] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOn(!isOn)}>
        {isOn ? "ON" : "OFF"}
      </button>

      <p>The switch is {isOn ? "enabled" : "disabled"}.</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The best initial value is usually:
useState(false)
Because most switches begin in the OFF state unless the product requirement says otherwise.

Real-time use cases

Enable/disable notifications.

Dark mode switch.

Show password / hide password.

Turn on location access.

Online / offline availability.

Email subscription toggle.

Open / close mobile menu.
Enter fullscreen mode Exit fullscreen mode

Your theme toggle is a real example:
checked={theme === "dark"}
onChange={toggleTheme}

The checkbox is checked only when the current theme is "dark".

2. Input Field Value: Why Use State?

Suppose the user types Vijay, and you want to display:
Hello, Vijay
const [name, setName] = useState("");

<input
  value={name}
  onChange={(e) => setName(e.target.value)}
  placeholder="Enter your name"
/>

<p>Hello, {name}</p>
Enter fullscreen mode Exit fullscreen mode

This is called a controlled input because React state controls the value of the input.

Why not use a normal variable?
This will not work correctly:

let name = "";

function handleChange(e) {
  name = e.target.value;
}
Enter fullscreen mode Exit fullscreen mode

The variable changes in JavaScript memory, but React does not know that it must update the UI. Therefore:
<p>Hello, {name}</p>
will not reliably show the latest name.
A normal variable also gets created again when the component re-renders. It is not a reliable place for UI data that must persist between renders.

The main difference

Normal variable useState
Value can change in JavaScript Value can change in React
Does not request a re-render Triggers a re-render
UI may not update UI updates with the latest value
Not reliable for interactive UI data Best for changing UI data

Your password input already follows this idea:

const [user, setUser] = useState("");

<input
  onInput={(e) => setUser(e.target.value)}
  type="password"
/>

<p>hello, {user}</p>
Enter fullscreen mode Exit fullscreen mode

A common React style is to use onChange:
onChange={(e) => setUser(e.target.value)}
Both can work in this case, but onChange is generally the standard choice for React form fields.
Real-time use cases
Search boxes.

  • Login and registration forms.
  • Live preview of a profile name.
  • Chat message typing.
  • Product filters.
  • Coupon code input.
  • Address forms.
  • Comment boxes.

3. Show / Hide Text

For showing or hiding one element, boolean state is the best choice.

const [showText, setShowText] = useState(false);
Then render the paragraph only when showText is true:

<button onClick={() => setShowText(!showText)}>
  {showText ? "Hide Text" : "Show Text"}
</button>

{showText && <p>This paragraph is visible now.</p>}
Enter fullscreen mode Exit fullscreen mode

React supports conditional rendering using normal JavaScript conditions, including if, ternary operators, and &&
Why boolean is suitable
The question is simple:
Should this item be visible?
There are only two answers:

Yes → true

No → false
So a boolean is cleaner than using strings such as "show" and "hide".

Your password visibility code is the same pattern:

const [showText, setShowText] = useState(false);

const toggle = () => {
  setShowText(!showText);
};
Enter fullscreen mode Exit fullscreen mode

<input type={showText ? "text" : "password"} />

When showText is:

  • false → password is hidden.
  • true → password is visible.

Real-time use cases

  • Password visibility.
  • FAQ accordion.
  • Sidebar open / close.
  • Popup modal open / close.
  • Read more / read less.
  • Notification panel.
  • Mobile navigation menu.
  • Loading screen visibility. **

4. Character Counter

**
For a textarea, store the actual text in state:
const [message, setMessage] = useState("");
Then calculate the length from that text:

<textarea
  value={message}
  onChange={(e) => setMessage(e.target.value)}
  placeholder="Write your message..."
/>

<p>Character count: {message.length}</p>
Enter fullscreen mode Exit fullscreen mode

If you want to ignore spaces at the beginning and end:
<p>Character count: {message.trim().length}</p>
Your code already does this:
<p>
character Count: {user.trim().length}
</p>

Should text and length both be in state?
Usually, no.

Do this:

const [text, setText] = useState("");
const length = text.length;
Enter fullscreen mode Exit fullscreen mode

Avoid this:
const [text, setText] = useState("");
const [length, setLength] = useState(0);

Why? length can always be calculated from text. It is derived data.

If you store both separately, they can become inconsistent:

text = "Hello"
length = 2
Enter fullscreen mode Exit fullscreen mode

That is incorrect state. Keeping only the text as state avoids this issue.

Real-time use cases

  • Social media post character limit.
  • Bio character count.
  • SMS character limit.
  • Product review textarea.
  • Tweet-like post composer.
  • Job application description.
  • Comment section.
  • AI prompt input limit. Example with a limit:
const [bio, setBio] = useState("");
const maxLength = 100;

<textarea
  value={bio}
  maxLength={maxLength}
  onChange={(e) => setBio(e.target.value)}
/>

<p>
  {bio.length} / {maxLength}
</p>
Enter fullscreen mode Exit fullscreen mode

5. Form with Multiple Inputs

For a form with related fields such as name, email, phone, city, and password, one object state is usually clean and scalable.

const [form, setForm] = useState({
  name: "",
  email: "",
  phone: "",
  city: "",
  password: "",
});
Enter fullscreen mode Exit fullscreen mode

Use one reusable change handler:
`const handleChange = (e) => {
const { name, value } = e.target;

setForm({
...form,
name: value,
});
};`
Input example:

<input
  type="text"
  name="name"
  value={form.name}
  onChange={handleChange}
  placeholder="Enter your name"
/>

<input
  type="email"
  name="email"
  value={form.email}
  onChange={handleChange}
  placeholder="Enter your email"
/>
Enter fullscreen mode Exit fullscreen mode

Your form implementation is already following this correct pattern.
Five separate states:

const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [city, setCity] = useState("");
const [password, setPassword] = useState("");
Enter fullscreen mode Exit fullscreen mode

This is valid, but it becomes repetitive as the form grows.
One object is easier for:

  • Form submission.
  • Sending data to an API.
  • Resetting the entire form.
  • Showing a preview.
  • Adding more fields later.

For example:
console.log(JSON.stringify(form));
That produces a single object ready to send to a backend API:

{
  "name": "Vijay",
  "email": "vijay@email.com",
  "phone": "9876543210",
  "city": "Chennai",
  "password": "secret123"
}
Enter fullscreen mode Exit fullscreen mode

Common mistake: replacing the full object
This is wrong:
`setForm({

});`
If the user updates only the email field, all other fields disappear.

For example, before:
{
name: "Vijay",
email: "",
phone: "9876543210"
}

Wrong update:
setForm({
email: "vijay@email.com",
});

Result:

{
email: "vijay@email.com"
}

The name and phone values are lost.

Correct update:

setForm((previousForm) => ({
  ...previousForm,
  [name]: value,
}));
Enter fullscreen mode Exit fullscreen mode

Using the previous-state callback is especially safe when the new state depends on the old state. React also documents that state setters can receive an updater function based on the previous state.

Better version of your handler

const handleChange = (e) => {
  const { name, value } = e.target;

  setForm((previousForm) => ({
    ...previousForm,
    [name]: value,
  }));
};
Enter fullscreen mode Exit fullscreen mode

Also, use this for the phone field:
type="tel"
Not:
type="tell"
Correct version:

<input
  type="tel"
  name="phone"
  value={form.phone}
  placeholder="Enter your phone"
  onChange={handleChange}
/>
Enter fullscreen mode Exit fullscreen mode

Real-time use cases
Registration form.

  • Checkout and delivery address.
  • Employee profile form.
  • Bank account application.
  • Contact us form.
  • Resume builder.
  • School admission form.
  • Admin dashboard product form.
  1. Checkbox Selection List For multiple selected skills, use an array state. const [skills, setSkills] = useState([]); Why an array? Because a user may select multiple items:
["Reactjs", "Nodejs", "SQL"]`
When a checkbox is checked, add the skill. When unchecked, remove it.
`const handleSkillChange = (e) => {
  const { value, checked } = e.target;

  setSkills((previousSkills) =>
    checked
      ? [...previousSkills, value]
      : previousSkills.filter((skill) => skill !== value)
  );
};
Enter fullscreen mode Exit fullscreen mode

This is the same logic you used:

setSkills(
  checked
    ? [...skills, value]
    : skills.filter((skill) => skill !== value)
);
Enter fullscreen mode Exit fullscreen mode

The callback version is preferred:

setSkills((previousSkills) =>
  checked
    ? [...previousSkills, value]
    : previousSkills.filter((skill) => skill !== value)
);
Enter fullscreen mode Exit fullscreen mode

How it works
If the user selects React:
[]
becomes:
["Reactjs"]
Then selects Node.js:
["Reactjs"]
becomes:
["Reactjs", "nodejs"]

Then unselects React:
["Reactjs", "nodejs"]

becomes:
["nodejs"]

import { useState } from "react";

function Skills() {
  const [skills, setSkills] = useState([]);

  const handleSkillChange = (e) => {
    const { value, checked } = e.target;

    setSkills((previousSkills) =>
      checked
        ? [...previousSkills, value]
        : previousSkills.filter((skill) => skill !== value)
    );
  };

  return (
    <div>
      <label>
        <input
          type="checkbox"
          value="React"
          onChange={handleSkillChange}
        />
        React
      </label>

      <label>
        <input
          type="checkbox"
          value="Node"
          onChange={handleSkillChange}
        />
        Node
      </label>

      <label>
        <input
          type="checkbox"
          value="Java"
          onChange={handleSkillChange}
        />
        Java
      </label>

      <label>
        <input
          type="checkbox"
          value="SQL"
          onChange={handleSkillChange}
        />
        SQL
      </label>

      <h3>Selected skills</h3>

      <ul>
        {skills.map((skill) => (
          <li key={skill}>{skill}</li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Notice this important improvement:
<li key={skill}>{skill}</li>
When rendering a list with .map(), React needs a stable key for each item. Since every selected skill is unique here, the skill text can be used as the key.

Real-time use cases

  • Select technical skills in a job portal.
  • Product category filters.
  • Choose food preferences.
  • Select permissions for users.
  • Pick multiple courses.
  • E-commerce filter: size, color, brand.
  • Multi-select notification settings.
  • Interests in a dating or social app.
  1. Dependent Dropdown: Country → State → City Dependent dropdown means that the second dropdown depends on the first, and the third depends on the second. Example:
Country → India
State   → Tamil Nadu
City    → Chennai
Enter fullscreen mode Exit fullscreen mode
const locationData = {
  India: {
    "Tamil Nadu": ["Chennai", "Madurai", "Coimbatore"],
    Kerala: ["Kochi", "Trivandrum"],
  },

  USA: {
    California: ["Los Angeles", "San Diego"],
    Texas: ["Houston", "Dallas"],
  },
};
Enter fullscreen mode Exit fullscreen mode

Use three state variables:

const [country, setCountry] = useState("");
const [state, setState] = useState("");
const [city, setCity] = useState("");
Enter fullscreen mode Exit fullscreen mode

This is clear because each selected value has a separate meaning.

When country changes
When the user changes country:

  • Update the country.
  • Reset state to an empty string.
  • Reset city to an empty string.
onChange={(e) => {
  setCountry(e.target.value);
  setState("");
  setCity("");
}}
Enter fullscreen mode Exit fullscreen mode

Why reset them?

Imagine this situation:
Country: India
State: Tamil Nadu
City: Chennai

Then the user selects:
Country: USA
Now Tamil Nadu and Chennai do not belong to the USA. So we must clear them.

When state changes
When the user selects a new state, reset only the city:
onChange={(e) => {
setState(e.target.value);
setCity("");
}}

Cleaner controlled dropdown version

<select
  value={country}
  onChange={(e) => {
    setCountry(e.target.value);
    setState("");
    setCity("");
  }}
>
  <option value="">Select country</option>

  {Object.keys(locationData).map((countryName) => (
    <option key={countryName} value={countryName}>
      {countryName}
    </option>
  ))}
</select>
Enter fullscreen mode Exit fullscreen mode
{country && (
  <select
    value={state}
    onChange={(e) => {
      setState(e.target.value);
      setCity("");
    }}
  >
    <option value="">Select state</option>

    {Object.keys(locationData[country]).map((stateName) => (
      <option key={stateName} value={stateName}>
        {stateName}
      </option>
    ))}
  </select>
)}
Enter fullscreen mode Exit fullscreen mode
{state && (
  <select value={city} onChange={(e) => setCity(e.target.value)}>
    <option value="">Select city</option>

    {locationData[country][state].map((cityName) => (
      <option key={cityName} value={cityName}>
        {cityName}
      </option>
    ))}
  </select>
)}
Enter fullscreen mode Exit fullscreen mode

The expressions:
{country && (...)}
and:
{state && (...)}
are conditional rendering. The state dropdown appears only after choosing a country; the city dropdown appears only after choosing a state. React supports this type of conditional UI using normal JavaScript logic.
Real-time use cases

  • Country → State → City in an address form.
  • Category → Subcategory → Product.
  • Company → Department → Employee.
  • University → Department → Course.
  • Vehicle brand → Model → Variant.
  • E-commerce category → type → item.
  • Travel booking: country → city → hotel.
  • Admin panel: role → permission → feature.

Top comments (0)