What is useState() and How Does it Work?

React

readTime

8 min

What is useState() and How Does it Work?

React introduced Hooks in version 16.8, transforming how developers manage state and side effects in functional components. One of the most foundational and widely used hooks is useState.

In this article, I'll break down:

  • What is useState
  • How it works
  • How to use it in different scenarios
  • Best practices for using useState()
  • Common mistakes to avoid
  • Example interview tasks that utilize useState()

What are Hooks in React?

Hooks are special functions in React that let you "hook into" state and lifecycle features in functional components. Introduced in React 16.8, they revolutionized how developers manage state, making functional components more powerful and easier to maintain.

πŸš€ Why are Hooks the core of React now?

  • Simplicity: Hooks allow state and lifecycle features to be used without writing class components. Functional components become more readable and easier to manage.
  • Reusability: You can create custom hooks, which lets you share logic between different components.
  • Flexibility: Hooks give you more control over state and side effects, making components more flexible and intuitive to use.

Why Do We Need Hooks?

Before hooks, managing state in React was restricted to class components. Hooks, like useState, allow you to:

  • Manage state without needing class components.
  • Avoid the complexity of lifecycle methods.
  • Simplify logic sharing across different components.

What is useState?

useState is a hook that allows you to add state to functional components. It enables managing state variables in components that previously required class components.


How Does useState Work?

Here’s the basic syntax for useState:

javascript
const [state, setState] = useState(initialState);
  • state: The current state value.
  • setState: A function to update the state.
  • initialState: The initial state value.

useState returns an array containing two elements: the current state and a function to update it. Using array destructuring, we can assign them to any variable names we choose.


Basic Example of useState

Let’s start with a simple example: a counter that increments by 1 each time a button is clicked.

Example Without useState:

javascript
import React from "react";
import ReactDOM from "react-dom";

let count = 0;

function increase() {
  count++;
  ReactDOM.render(
    <div className="container">
      <h1>{count}</h1>
      <button onClick={increase}>+</button>
    </div>,
    document.getElementById("root")
  );
}

ReactDOM.render(
  <div className="container">
    <h1>{count}</h1>
    <button onClick={increase}>+</button>
  </div>,
  document.getElementById("root")
);

In this code, we manually re-render the component every time the button is clicked, which is inefficient and against React's principles.

Example With useState:

javascript
import React, { useState } from "react";

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

  function increase() {
    setCount(count + 1);
  }

  return (
    <div className="container">
      <h1>{count}</h1>
      <button onClick={increase}>+</button>
    </div>
  );
}

With useState, we manage the counter state efficiently, automatically re-rendering the UI when the state changes.


Deeper Understanding of useState

Initializing State

The state declared by useState can be initialized not just with primitive values but also with objects or even functions.

javascript
const [user, setUser] = useState({ name: "John", age: 30 });

You can also pass a function to useState to compute the initial state only once, during the first render.

javascript
const [count, setCount] = useState(() => computeExpensiveInitialState());

Updating State

The setState function is used to update the state. There are two main ways to update state:

  1. Directly setting a new state value:

    javascript
    setCount(count + 1);
    
  2. Using an updater function (when the new state depends on the previous state):

    javascript
    setCount((prevCount) => prevCount + 1);
    

Managing Complex State

If the state is an object with multiple properties, be cautious not to overwrite the entire object when updating a single property.

javascript
const [state, setState] = useState({ name: "John", age: 30 });

function updateName(newName) {
  setState((prevState) => ({
    ...prevState,
    name: newName,
  }));
}

Advanced useState Examples

Light Switch Example

Here’s an example of a light switch component that toggles the background color between white and black.

javascript
import React, { useState } from "react";

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

  function toggleLight() {
    setIsOn(!isOn);
  }

  return (
    <div
      style={{
        height: "100vh",
        background: isOn ? "white" : "black",
        color: isOn ? "black" : "white",
      }}
      onClick={toggleLight}
    >
      {isOn ? "Light is On" : "Light is Off"}
    </div>
  );
}

Login Form Example

Here’s a simple login form using useState to manage the input fields.

javascript
import React, { useState } from "react";

function LoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  function handleSubmit(event) {
    event.preventDefault();
    alert(`Email: ${email}, Password: ${password}`);
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Email:</label>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </div>
      <div>
        <label>Password:</label>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </div>
      <button type="submit">Login</button>
    </form>
  );
}

Real-Time Clock Example

This example creates a clock that shows the current time, updating every second.

javascript
import React, { useState, useEffect } from "react";

function Clock() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const timer = setInterval(() => setTime(new Date()), 1000);
    return () => clearInterval(timer); // Clean up setInterval on unmount
  }, []);

  return <h1>{time.toLocaleTimeString()}</h1>;
}

Optimization and Best Practices

Avoiding Unnecessary Renders

When using setState, avoid unnecessary component renders by ensuring state is updated only when necessary.

javascript
function increment() {
  if (count < 10) {
    setCount(count + 1);
  }
}

Combining Related States

While you can use multiple useState hooks to manage different state variables, consider combining them into one object if the states are closely related.

javascript
const [formState, setFormState] = useState({
  name: "",
  email: "",
  password: "",
});

function updateFormState(field, value) {
  setFormState({
    ...formState,
    [field]: value,
  });
}

Debugging

Using hooks like useState can make state debugging tricky. Use tools like React Developer Tools to track component states easily.


Common Mistakes When Using useState

1. Updating State Without setState

A common mistake is attempting to update the state directly instead of using the setState function.

Wrong:

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

function wrongIncrement() {
  count++; // ❌ Directly modifying state
}

Correct:

javascript
function correctIncrement() {
  setCount(count + 1); // βœ… Using setState
}

2. Forgetting to Initialize State

Initializing state in useState is crucial. You cannot call useState without providing an initial value.

Wrong:

javascript
const [count, setCount] = useState(); // ❌ No initial value

Correct:

javascript
const [count, setCount] = useState(0); // βœ… Initial value set

3. Ignoring Previous State When Updating

When the new state depends on the previous state, use an updater function to avoid errors.

Wrong:

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

function increment() {
  setCount(count + 1); // ❌

 Can cause issues in certain cases
}

Correct:

javascript
function safeIncrement() {
  setCount((prevCount) => prevCount + 1); // βœ… Safer approach using previous state
}

4. Using useState Outside Functional Components

Hooks must be used inside functional components or other hooks. They cannot be called in regular functions or outside components.

Wrong:

javascript
function useStateOutsideComponent() {
  const [state, setState] = useState(0); // ❌ Not allowed outside a component
}

Correct:

javascript
function MyComponent() {
  const [state, setState] = useState(0); // βœ… Correct usage inside a functional component
  return <div>{state}</div>;
}

Best Practices for useState

1. Group Related States into One Object

When managing related pieces of state, consider combining them into one object.

javascript
const [user, setUser] = useState({ name: "", age: 0 });

function updateUser(field, value) {
  setUser((prevUser) => ({
    ...prevUser,
    [field]: value,
  }));
}

2. Separate Logic from the Component

To keep your component clean, move state logic to external functions or custom hooks.

javascript
function useCounter(initialValue) {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  return { count, increment, decrement };
}

function CounterComponent() {
  const { count, increment, decrement } = useCounter(0);

  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  );
}

3. Use Built-in React Tools for Debugging

React Developer Tools make it easier to track state in components. Utilize these tools to streamline your debugging process.

4. Avoid Overly Complex State in a Single Hook

If the state becomes too complex, consider splitting it into multiple useState calls or using useReducer for more intricate logic.

javascript
const [name, setName] = useState("");
const [age, setAge] = useState(0);

5. Optimize Memory Usage

When the initial state is the result of an expensive calculation, pass a function to useState to compute the initial value only on the first render.

javascript
const [expensiveState, setExpensiveState] = useState(() =>
  computeExpensiveState()
);

Tricky Situations with useState

1. Asynchronous Updates

State updates in React are asynchronous, which can lead to tricky situations if you try to use the updated state immediately after setting it.

Wrong:

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

function handleClick() {
  setCount(count + 1);
  console.log(count); // ❌ May not show the updated value
}

Correct:

javascript
useEffect(() => {
  console.log(count); // βœ… Will always show the updated value
}, [count]);

2. Side Effects in the Render Function

Avoid putting setState or other side effects directly inside the render function, as this can lead to infinite render loops.

Wrong:

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

function App() {
  setCount(count + 1); // ❌ Causes infinite render loop
  return <h1>{count}</h1>;
}

Correct:

javascript
function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1);
  }, []);

  return <h1>{count}</h1>;
}

Conclusion

useState is one of the most important tools in React, allowing you to add state to functional components. It lets you manage and update state in a clear and effective way.

From simple counters to complex forms and dynamic interfaces, useState provides the flexibility needed to build interactive applications. Hooks like useState change the way we code in React, making components more modular and easier to understand.

If you want to learn more about hooks, check out other popular hooks like useEffect and useContext.

I hope this article helped you understand what useState is and how to effectively use it in your React projects!

authorImg

Witek Pruchnicki

I passionately share knowledge about programming and more in various ways.