Managing State in react using different method, & understand batching

Roman - Sep 23 - - Dev Community

React’s component-based architecture revolves around the concept of state. Understanding how to effectively initialize, update, and manage state is crucial for building robust and performant React applications. In this comprehensive guide, we’ll explore various methods of state management in React, focusing on functional components and hooks. We’ll cover everything from basic state initialization to advanced state management techniques, including the batching process.

useState(): The Building Block of State Management

The useState hook is the most basic and commonly used method for managing the state in functional components. It’s perfect for simple state management scenarios.

const [state, setState] = useState(initialState);
Enter fullscreen mode Exit fullscreen mode

Shopping Cart Item Counter Example:

Let’s create a simple shopping cart item counter using useState:

import React, { useState } from 'react';

function ShoppingCartCounter() {
  const [itemCount, setItemCount] = useState(0);

  const addItem = () => setItemCount(prevCount => prevCount + 1);
  const removeItem = () => setItemCount(prevCount => Math.max(0, prevCount - 1));

  return (
    <div>
      <h2>Shopping Cart</h2>
      <p>Items in cart: {itemCount}</p>
      <button onClick={addItem}>Add Item</button>
      <button onClick={removeItem}>Remove Item</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we use useState to initialize and manage the itemCount state. The setItemCount function allows us to update the state based on user interactions.

useReducer(): Managing Complex State Logic

When state logic becomes more complex or when the next state depends on the previous one, useReducer can be a more suitable choice.

Todo List Example:

Let’s create a more complex todo list application using useReducer:

import React, { useReducer } from 'react';

// Reducer function
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.payload, completed: false }];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
      );
    case 'DELETE_TODO':
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
}

function TodoList() {
  const [todos, dispatch] = useReducer(todoReducer, []);
  const [inputText, setInputText] = React.useState('');

  const addTodo = () => {
    if (inputText.trim()) {
      dispatch({ type: 'ADD_TODO', payload: inputText });
      setInputText('');
    }
  };

  return (
    <div>
      <h2>Todo List</h2>
      <input
        value={inputText}
        onChange={(e) => setInputText(e.target.value)}
        placeholder="Enter a new todo"
      />
      <button onClick={addTodo}>Add Todo</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span
              style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
              onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
            >
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

useContext(): Sharing State Across Components

The useContext hook, combined with the Context API, allows you to share state across multiple components without explicitly passing props through every level of the component tree.

Theme Switcher Example:

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

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

// Theme 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>
  );
}

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

  return (
    <button
      onClick={toggleTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff',
      }}
    >
      Toggle Theme
    </button>
  );
}

// Another component that uses the theme
function ThemedText() {
  const { theme } = useContext(ThemeContext);

  return (
    <p style={{ color: theme === 'light' ? '#333' : '#fff' }}>
      Current theme: {theme}
    </p>
  );
}

// Main app component
function App() {
  return (
    <ThemeProvider>
      <div>
        <h1>Theme Switcher Example</h1>
        <ThemedText />
        <ThemedButton />
      </div>
    </ThemeProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Redux Toolkit: Centralized State Management:

For larger applications with complex state management needs, Redux Toolkit offers a powerful solution. It provides utilities to simplify common Redux use cases, including store setup, defining reducers, immutable update logic, and even creating entire “slices” of state at once.

E-commerce Cart Management:

import React from 'react';
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';

// Create a slice for cart state
const cartSlice = createSlice({
  name: 'cart',
  initialState: [],
  reducers: {
    addItem: (state, action) => {
      const existingItem = state.find(item => item.id === action.payload.id);
      if (existingItem) {
        existingItem.quantity += 1;
      } else {
        state.push({ ...action.payload, quantity: 1 });
      }
    },
    removeItem: (state, action) => {
      const index = state.findIndex(item => item.id === action.payload);
      if (index !== -1) {
        if (state[index].quantity > 1) {
          state[index].quantity -= 1;
        } else {
          state.splice(index, 1);
        }
      }
    },
  },
});

// Create the store
const store = configureStore({
  reducer: {
    cart: cartSlice.reducer,
  },
});

// Extract action creators
const { addItem, removeItem } = cartSlice.actions;

// Cart component
function Cart() {
  const cartItems = useSelector(state => state.cart);
  const dispatch = useDispatch();

  return (
    <div>
      <h2>Shopping Cart</h2>
      {cartItems.map(item => (
        <div key={item.id}>
          <span>{item.name} - Quantity: {item.quantity}</span>
          <button onClick={() => dispatch(addItem(item))}>+</button>
          <button onClick={() => dispatch(removeItem(item.id))}>-</button>
        </div>
      ))}
    </div>
  );
}

// Product list component
function ProductList() {
  const dispatch = useDispatch();
  const products = [
    { id: 1, name: 'Product 1', price: 10 },
    { id: 2, name: 'Product 2', price: 15 },
    { id: 3, name: 'Product 3', price: 20 },
  ];

  return (
    <div>
      <h2>Products</h2>
      {products.map(product => (
        <div key={product.id}>
          <span>{product.name} - ${product.price}</span>
          <button onClick={() => dispatch(addItem(product))}>Add to Cart</button>
        </div>
      ))}
    </div>
  );
}

// Main app component
function App() {
  return (
    <Provider store={store}>
      <div>
        <h1>E-commerce App</h1>
        <ProductList />
        <Cart />
      </div>
    </Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Understanding the Batching Process

Before we dive into the batching process, let’s understand the problem it solves. In React, state updates trigger re-renders of components. When multiple state updates occur in rapid succession, it can lead to unnecessary re-renders, affecting performance.

Multiple Re-renders Example:

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

  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  };

  console.log('Render');

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, you might expect the count to increase by 3 when the button is clicked. However, it only increases by 1. This is because React batches the state updates that occur in event handlers. The console.log('Render') would only appear once in the console, indicating a single re-render.

The Solution: Automatic Batching

React 18 introduced automatic batching for all updates, regardless of where they originate. This means that multiple state updates are grouped together, resulting in a single re-render.

Here’s how you can leverage batching for better performance:

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

  const handleClick = () => {
    setCount(c => c + 1);
    setFlag(f => !f);
    // These updates will be batched together
  };

  console.log('Render');

  return (
    <div>
      <p>Count: {count}</p>
      <p>Flag: {flag.toString()}</p>
      <button onClick={handleClick}>Update</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The article is originally posted at Programmingly.dev, read the full article at following link
https://programmingly.dev/managing-state-in-react-using-different-method-understand-batching

. . . . . . . . . . . . . . . . . . .