DEV Community

Cover image for Do You Really Need "useState" for Everything? Exploring Alternatives
Ishan Bagchi for Byte-Sized News

Posted on • Originally published at Medium

Do You Really Need "useState" for Everything? Exploring Alternatives

When you first dive into React, useState feels like the magic spell that makes everything work. Want a button to track clicks? Use useState. Need to toggle a modal? useState again. But as you get deeper into React development, you might start wondering: Is useState the right choice for every situation?

The answer, unsurprisingly, is no. While useState is versatile, React offers other hooks and patterns that might be a better fit depending on your specific needs. Let's explore some alternatives like useRef, useReducer, and useContext to see when they shine.

When to Use useRef Instead of useState

A classic React beginner mistake is using useState for values that don't actually affect rendering. useRef is an ideal choice when you need to persist data across renders without triggering a re-render.

A Practical Example:

Imagine you’re tracking how many times a button is clicked, but you don't need the component to re-render every time.

function ClickTracker() {
  const clickCount = useRef(0);

  const handleClick = () => {
    clickCount.current += 1;
    console.log(`Button clicked ${clickCount.current} times`);
  };

  return <button onClick={handleClick}>Click me</button>;
}
Enter fullscreen mode Exit fullscreen mode

In this case, useRef holds the click count without causing unnecessary re-renders. If you used useState, the component would re-render with each click, which isn't necessary here.

When to Choose useRef:

  • Tracking values that don’t need to trigger a UI update.
  • Storing references to DOM elements or previous state values.

When useReducer Shines Over useState

For more complex state logic, especially when your state involves multiple sub-values or actions, useReducer can be a powerful alternative. useState might start feeling clunky when you're managing several interdependent pieces of state.

A Real-World Scenario:

Suppose you're building a form where you manage several inputs like name, email, and password. Using useState for each input can quickly become tedious.

function formReducer(state, action) {
  switch (action.type) {
    case 'SET_NAME':
      return { ...state, name: action.payload };
    case 'SET_EMAIL':
      return { ...state, email: action.payload };
    case 'SET_PASSWORD':
      return { ...state, password: action.payload };
    default:
      return state;
  }
}

function SignupForm() {
  const [formState, dispatch] = useReducer(formReducer, {
    name: '',
    email: '',
    password: ''
  });

  return (
    <>
      <input
        value={formState.name}
        onChange={(e) => dispatch({ type: 'SET_NAME', payload: e.target.value })}
        placeholder="Name"
      />
      <input
        value={formState.email}
        onChange={(e) => dispatch({ type: 'SET_EMAIL', payload: e.target.value })}
        placeholder="Email"
      />
      <input
        value={formState.password}
        onChange={(e) => dispatch({ type: 'SET_PASSWORD', payload: e.target.value })}
        placeholder="Password"
      />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, useReducer centralizes all the state updates into a single function, making it easier to manage than multiple useState calls.

When to Choose useReducer:

  • Handling complex state logic with multiple sub-values or actions.
  • When state transitions follow a clear, action-based flow (e.g., SET, ADD, REMOVE).

Should You Reach for useContext Instead?

If your state is shared across many components, prop drilling can quickly become a nightmare. That's where useContext comes in—it helps you share state without passing props down multiple levels.

A Contextual Example:

Imagine you're building a shopping cart. You need the cart's state (items added, total price, etc.) to be accessible in different parts of the app—maybe the header, the checkout page, and the cart preview.

const CartContext = React.createContext();

function CartProvider({ children }) {
  const [cart, setCart] = useState([]);

  return (
    <CartContext.Provider value={{ cart, setCart }}>
      {children}
    </CartContext.Provider>
  );
}

function Header() {
  const { cart } = React.useContext(CartContext);
  return <div>Items in cart: {cart.length}</div>;
}

function App() {
  return (
    <CartProvider>
      <Header />
      {/* Other components */}
    </CartProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this scenario, useContext makes the cart state available to any component that needs it without manually passing props.

When to Choose useContext:

  • Sharing state between deeply nested components.
  • Avoiding prop drilling for commonly accessed global data (e.g., user authentication, themes).

A Balanced Approach

While useState is a great starting point, React's ecosystem offers other powerful tools like useRef, useReducer, and useContext that can simplify your code and improve performance. Instead of reaching for useState by default, ask yourself a few key questions:

  • Does this state need to trigger a re-render? (If not, consider useRef)
  • Is my state logic becoming too complex for useState? (Try useReducer)
  • Am I passing down props through too many components? (Look into useContext)

By choosing the right tool for the job, you'll write more efficient, maintainable React components that are easier to reason about.

So, next time you find yourself defaulting to useState, pause for a moment. Maybe there’s a better way to handle things!

Top comments (0)