How to use React useState() in React

How to use React useState() in React

Mastering State in React Functional Components

·

9 min read

Getting Hooked on useState()

Hooks were introduced in React as a way to use state and other React features without writing classes. The useState() Hook is one of the most basic but powerful for adding state to functional components.

React's useState hook allows you to add state to functional components. This makes state accessible without needing a class component.

In this comprehensive guide, you will learn:

  • What state is and why it's useful

  • How to declare state variables with useState

  • When to use different state update methods

  • Real world examples for managing state

  • Updating state immutably

  • Common bugs and how to avoid them

Introduction to State

Before hooks, the state was only available in class components in React. The state allows you to store data that persists across component renders.

For example, imagine we have a Counter component that needs to track a count:

// Without state:
function Counter() {
  let count = 0; // Reset to 0 on each render

  return <div>Count: {count}</div>;
}

The count would reset back to 0 on each render - so we can't increment a counter this way.

With state, we can persist data between renders:

// With state:

function Counter() {
  const [count, setCount] = useState(0); // Persisted state

  return <div>Count: {count}</div>; 
}

Now the count variable will persist across renders. This gives us memory in function components!

State is essential for many use cases like form input values, caching fetched data, loading state, and more.

Declaring State Variables

To add state to a function component, first import the useState hook:

import { useState } from 'react';

Then declare state variables inside your component by calling useState:

function MyComponent() {
  // Declare count state variable
  const [count, setCount] = useState(0); 
}

useState accepts an initial state value and returns an array:

  • First item is the current state value

  • Second is a setter function to update it

By convention, name the state value something and the setter setSomething.

You can have many state variables by calling useState multiple times:

const [count, setCount] = useState(0);
const [name, setName] = useState('Mary');

When to Use Different Update Methods

There are a few ways to update state variables:

// Directly pass a new value 
setCount(10);

// Pass a callback function
setCount(prevCount => prevCount + 1);

Use passing a direct value when:

  • The update doesn't depend on previous state

  • You have the desired state value already

For example, updating state from an input field or toggling a boolean flag.

Use a callback function when:

  • Updating state based on previous state

  • Updating objects or arrays immutably

For example, incrementing a value, editing an object item in an array.

Real World Examples

Here are some common real-world use cases for managing state with useState:

Page/View State

It's common to store current page or view state in React apps:

// Track current page
const [currentPage, setCurrentPage] = useState('Home');

// Toggle between views:
const togglePage = () => {
  setCurrentPage(page === 'Home' ? 'About' : 'Home');
}

return (
  <div>
    {currentPage === 'Home' ? <HomePage /> : <AboutPage />}
  </div>
);

This allows changing the visible page/view.

Loading State

You can store component loading state in React:

function MyComponent() {
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    fetchData().then(() => setLoading(false)); 
  }, []);

  if (loading) return <Loader />

  // ...
}

Then conditionally show loading indicators based on state.

Tracking Network Requests

We can track network request state:

const [status, setStatus] = useState('idle');

function fetchData() {
  setStatus('pending');
  fetch('/data')
    .then(res => {
      setStatus('success');
    })
    .catch(() => {
      setStatus('error');
    })
}

This allows showing different UI based on the request status.

Managing Modals/Overlays

Use state to open/close modal overlays:

const [showModal, setShowModal] = useState(false);

return (
  <>
    <button onClick={() => setShowModal(true)}>Open Modal</button>

    {showModal && (
      <Modal>
        <button onClick={() => setShowModal(false)}>Close</button>
      </Modal>  
    )}
  </>
)

Toggling the state will show/hide the modal overlay.

Updating State Immutably

One common pitfall is mutating state directly:

// ❌ Wrong
user.name = 'Mary'; 

// ✅ Correct
setUser({...user, name: 'Mary'});

You need to treat state as immutable and update it without mutation.

To update objects or arrays immutably, pass a callback function to create a new copy with changes rather than mutating:

setUsers(prevUsers => {
  // Copy prev array
  const updated = [...prevUsers];

  // Update copy
  updated[0] = { ...updated[0], isActive: true };

  // Return new array
  return updated;
});

This avoids issues from mutating the previous state directly.

Simple Counter Example

Let's look at a simple counter example to demonstrate useState() in action:

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

We initialize the count state to 0. The click handler increments the count by calling the setter function. This updates the count state and re-renders the component to display the latest value.

Shopping Cart

useState() can also manage arrays of data, like a shopping cart:

function ShoppingCart() {
  const [items, setItems] = useState([]);

  function addItem(product) {
    setItems(prevItems => [...prevItems, product]);
  }

  // Omitted: removeItem, render, etc
}

We initialize the items array with useState(), then add or remove items by pushing new values. The array setter merges the updated array instead of replacing so you don't lose existing items.

This makes useState() great for managing lists and collections!

Todo List

To demonstrate filtering array state, here is a TodoList example:

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all'); // 'all', 'active', or 'completed'

  // Omitted: addTodo, toggleComplete

  function displayTodos() {
    if (filter === 'all')      return todos;
    if (filter === 'active')   return todos.filter(t => !t.completed);
    if (filter === 'completed')return todos.filter(t => t.completed);
  }

  return (
    <>
      {/* Display todos based on filter */}
      {displayTodos().map(renderTodo)}

      {/* Toggler for filter state */}  
    </>
  );
}

We manage the todos array with useState(), then derive filtered lists based on a filter state variable. This makes it easy to update the UI based on changing state.

Forms

For storing form data, useState() is ideal:

function Form() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  return (
    <form>
      {/* Email input */}
      <input 
        value={email}
        onChange={e => setEmail(e.target.value)} 
      />

      {/* Password input */}
      <input
        value={password}        
        onChange={e => setPassword(e.target.value)}
      />
    </form>
  );
}

The form values are stored in state rather than directly on the inputs. This allows centralized form management, including validation and easy submission.

Troubleshooting in useState()

useState() is powerful but can also lead to issues if used incorrectly. Here are some common problems and ways to fix them:

Updates Not Rendering

Updates may not reflect if you mutate state directly instead of using the setter:

// ❌ Wrong
count++ 

// ✅ Correct
setCount(prevCount => prevCount + 1)

Always use the setter function to update state properly.

Stale State Values

The setter functions are asynchronous, so current state may be stale:

// ❌ Wrong
setCount(count + 1); // `count` is stale

// ✅ Correct
setCount(c => c + 1); // Pass function to avoid stale value

Pass a callback to ensure latest state is used for updates.

Too Many Re-renders

Every state change re-renders, so keep components lean:

// ❌ Causes re-render on each state change
function Component() {
  // Complex UI and logic
}

// ✅ Split components and extract logic
function ComponentUI() {
  // Only UI
} 

function ComponentLogic() {
  // Data handling
}

Move non-visual logic outside components to optimize rendering.

Merging State Incorrectly

State is merged on updates, not replaced:

// ❌ Replaces entirely 
setUser({name: 'John'});

// ✅ Merges update
setUser(user => ({...user, name: 'John'}));

Pass a callback merging previous state to avoid erasing state.

Forgetting to Call Setters

Don't update state directly - always use setters:

// ❌ Wrong 
user.name = 'John' 

// ✅ Correct
setUser(user => ({...user, name: 'John'}));

Setters update state - direct assignments won't work!

Here are some tips on performance optimization with useState() for more advanced React users:

Performance Optimizations

When dealing with complex components and state, there are some steps we can take to optimize performance:

1. Memoize State Setters

The setter functions from useState() can be memoized with useCallback to prevent unnecessary re-renders:

const setCount = useCallback(newCount => {
  setCount(newCount); 
}, []);

This avoids re-creating the setter on each render.

2. Move State Up

Lifting state to parent components when possible avoids passing callbacks down through intermediate children. This can improve rendering speed by minimizing re-renders.

3. Debounce State Changes

For state that changes rapidly like window events, debounce the setters:

const debouncedSetCount = useDebounce(setCount, 500);

// Debounced update every 500ms
window.addEventListener('scroll', debouncedSetCount);

This batches rapid updates into single renders.

4. Split Component Logic

Extract data fetching, parsing, or other complex logic outside the component to improve rendering:

function Component() {
  // Keep mostly UI
}

function dataLogic() {
  // Extract data handling 
}

Keeping components as lean as possible optimizes re-rendering.

These are some ways to optimize performance for complex useState() management! Let me know if you would like me to explain any of the concepts in more detail.

Here are some example interview questions on useState():

Q: What is the useState hook in React?

A: useState is a hook that allows function components to have state. It initializes the state and returns the current state value and a function to update it. useState is a simpler alternative to using class state.

Q: How do you update the state with useState?

Answer: Call the setter function returned from useState to update the state value. This triggers a re-render of the component with the new state. For example:

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

setCount(10); // Update count state to 10

Q: Why is directly mutating state a problem with useState?

Answer: React relies on state changes to trigger re-renders. If state is mutated directly, React won't know to re-render the component with the new state. Always use the setter function so React can track state changes.

Q: Fix the counter component so clicking the button increments the count:

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

  function handleClick() {
    count++; //direct mutation
  }

  return <button onClick={handleClick}>Increment</button> 
}

Answer:

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

  function handleClick() {
    setCount(prevCount => prevCount + 1);
  }

  return <button onClick={handleClick}>Increment</button>
}

Use the setter function to update count state properly.

Q: Optimize this component to prevent unnecessary re-renders:

function Chat() {
  const [count, setCount] = useState(0); // State change triggers re-render

  return <ChatLog count={count}/> 
}

// ChatLog re-renders even if props haven't changed

Answer:

// Only re-render if count changes
const ChatLog = React.memo(ChatLog); 

// Wrap setter in useCallback
const setCount = useCallback(newCount => {
  setCount(newCount);
}, []);

Memoize components and setters appropriately.

Recap

Some key benefits of useState():

  • Declarative state management without classes

  • Encapsulated state logic within components

  • State changes trigger re-renders to update UI

  • Flexible for various data types like strings, arrays, objects

  • Troubleshooting in useState()

  • Interviewed Questioned

We covered several practical examples like counters, lists, forms and more. useState() makes state in function components simple!

Try it in your next React project.

Summary

That covers everything you need to know about useState() - from basics to real-world examples, recap, and troubleshooting tips. useState() is a powerful tool for state management in function components.

Did you find this article valuable?

Support Mikey by becoming a sponsor. Any amount is appreciated!