State in React JS

As a developer, I was building a React application for a client who needed to track inventory for their small business in Texas. The application needed to update prices, quantities, and product availability in real-time as users interacted with it.

The challenge was managing all this changing data efficiently. That’s when I realized the true power of React’s state management.

In this article, I’ll walk you through everything you need to know about state in React JS, from basic concepts to advanced techniques.

So let’s get in!

State in React

State in React is a JavaScript object that holds data that may change over time and affects how a component renders. Think of state as the “memory” of your component.

Unlike props, which are passed down from parent components, state is managed within the component itself. When state changes, React re-renders the component to reflect those changes.

Here’s a simple example of state in a component:

import React, { useState } from 'react';

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

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

Check out Override a Styled Component in React

Method 1: Use the useState Hook

The useState hook is the simplest way to add state to functional components in React. It was introduced in React 16.8 and has become the standard approach.

Here are the steps to use useState:

  1. Import the useState hook from React
  2. Call useState with an initial value
  3. Destructure the returned array to get the state value and setter function
  4. Use the setter function to update the state

Let’s create a simple form that uses useState:

import React, { useState } from 'react';

function SignupForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isSubmitted, setIsSubmitted] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    // Form validation logic here
    setIsSubmitted(true);
  };

  return (
    <div>
      {isSubmitted ? (
        <p>Thanks for signing up!</p>
      ) : (
        <form onSubmit={handleSubmit}>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="Email"
            required
          />
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            placeholder="Password"
            required
          />
          <button type="submit">Sign Up</button>
        </form>
      )}
    </div>
  );
}

For multiple related state values, you can combine them into a single object:

const [formData, setFormData] = useState({
  email: '',
  password: '',
});

// Update using the spread operator
const handleChange = (e) => {
  setFormData({
    ...formData,
    [e.target.name]: e.target.value
  });
};

I executed the above example code and added the screenshot below.

State in React JS useState Hook

Read Build a Reusable Icon Component in React

Method 2: Use the useReducer Hook

When your state logic becomes complex, useReducer it provides a more structured approach. It’s inspired by Redux and follows the reducer pattern.

This is especially useful for:

  • Complex state logic with multiple sub-values
  • The state that depends on the previous state
  • Deep updates where you need to update nested properties

Here’s how to implement a shopping cart using useReducer:

import React, { useReducer } from 'react';

const initialState = {
  items: [],
  total: 0,
};

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload],
        total: state.total + action.payload.price,
      };
    case 'REMOVE_ITEM':
      const filteredItems = state.items.filter(
        item => item.id !== action.payload.id
      );
      return {
        ...state,
        items: filteredItems,
        total: filteredItems.reduce((sum, item) => sum + item.price, 0),
      };
    default:
      return state;
  }
}

function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, initialState);

  const addItem = (item) => {
    dispatch({ type: 'ADD_ITEM', payload: item });
  };

  const removeItem = (itemId) => {
    dispatch({ type: 'REMOVE_ITEM', payload: { id: itemId } });
  };

  return (
    <div>
      <h2>Shopping Cart</h2>
      <p>Total: ${cart.total.toFixed(2)}</p>

      <button onClick={() => addItem({ id: Date.now(), name: 'Product', price: 29.99 })}>
        Add Sample Product
      </button>

      <ul>
        {cart.items.map(item => (
          <li key={item.id}>
            {item.name} - ${item.price.toFixed(2)}
            <button onClick={() => removeItem(item.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

I executed the above example code and added the screenshot below.

State in React JS useReducer Hook

Read Props in React JS

Method 3: Use Context API for Global State

Sometimes you need to share state across many components. Props drilling (passing props through multiple levels) can become unwieldy. This is where Context API comes in.

Context provides a way to share values like state between components without explicitly passing props through every level of the component tree.

Here’s how to implement a theme switcher using Context:

import React, { createContext, useContext, useState } from 'react';

// Create a context
const ThemeContext = createContext();

// Create a provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Custom hook to use the theme context
function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// A component that uses the theme
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button 
      onClick={toggleTheme}
      style={{ 
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff',
        padding: '8px 16px',
        border: '1px solid #ccc',
        borderRadius: '4px'
      }}
    >
      Toggle Theme
    </button>
  );
}

// App component that uses the provider
function App() {
  return (
    <ThemeProvider>
      <div style={{ padding: '20px' }}>
        <h1>Theme Switcher Example</h1>
        <ThemedButton />
      </div>
    </ThemeProvider>
  );
}

I executed the above example code and added the screenshot below.

State in React JS Context API

Read Form Validation in React.js

State Management Best Practices

After working with React state for years, I’ve developed some best practices that help keep my code clean and maintainable:

  1. Keep state as local as possible: Only lift state when necessary. The closer the state is to where it’s used, the simpler your app will be.
  2. Use the right tool for the job:
  • useState for a simple state
  • useReducer for complex state logic
  • Context API for global state
  • Consider external libraries like Redux or Zustand for very complex applications
  1. Separate business logic from UI logic: Keep your component functions focused on rendering, and move complex logic to custom hooks or reducer functions.
  2. Avoid state duplication: Derive state when possible instead of keeping multiple copies that can get out of sync.
  3. Batch state updates: When making multiple state updates in response to a single event, consider using useReducer or batching updates.

Common State Management Mistakes to Avoid

Here are some pitfalls I’ve encountered and how to avoid them:

Directly Mutating State

// ❌ Wrong - Direct mutation
const handleClick = () => {
  user.name = 'New Name';
  setUser(user); // This won't trigger a re-render!
};

// ✅ Correct - Create a new object
const handleClick = () => {
  setUser({ ...user, name: 'New Name' });
};

Updating State Based on Previous State Incorrectly

// ❌ Wrong - Race condition risk
const increment = () => {
  setCount(count + 1);
};

// ✅ Correct - Using functional update
const increment = () => {
  setCount(prevCount => prevCount + 1);
};

Overusing Global State

It’s tempting to put everything in global state, but this can lead to performance issues and make your app harder to maintain. Keep state as local as possible and only lift it up when necessary.

Read Pass Props to a Component in React

Advanced State Patterns

Once you’re comfortable with the basics, you can explore more advanced patterns:

State Machines with XState

For complex state transitions, consider using a state machine library like XState. This helps formalize your state logic and prevents impossible states.

Optimistic UI Updates

For a more responsive user experience, update your UI immediately before waiting for server responses, then reconcile if there are errors.

const addTodo = async (text) => {
  // Create a temporary ID
  const tempId = Date.now();

  // Optimistically add the todo to state
  setTodos(prev => [...prev, { id: tempId, text, status: 'pending' }]);

  try {
    // Make the actual API call
    const response = await api.addTodo(text);

    // Replace the temporary todo with the real one from the server
    setTodos(prev => prev.map(todo => 
      todo.id === tempId ? { ...response.data, status: 'completed' } : todo
    ));
  } catch (error) {
    // On error, either remove the todo or mark it as failed
    setTodos(prev => prev.map(todo => 
      todo.id === tempId ? { ...todo, status: 'failed' } : todo
    ));
    // Show error to user
    setError('Failed to add todo');
  }
};

I hope you found this article helpful. State management is at the heart of React development, and mastering it will significantly improve your ability to build complex, interactive applications. Whether you’re building a simple form or a complex dashboard for a Fortune 500 company, these principles will help you write cleaner, more maintainable code.

Related tutorials:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.