Learnwizy Technologies Logo

Learnwizy Technologies

Class 19: State Management, Optimization and Asynchronous Operations

As React applications grow in complexity, managing state can become challenging. While the useState and useReducer Hooks are excellent for local component state, sharing state across many components (especially non-parent-child relationships) can lead to issues like "prop drilling" (passing props down through many layers of components).

This class explores advanced state management techniques, performance optimizations using memoization, and the common pattern of fetching asynchronous data from APIs.


Global State Management with Context API

In smaller applications, passing props down from parent to child components (prop drilling) works well. However, in larger apps, if many components deep in the tree need access to the same data, passing props through every intermediate component can become cumbersome and lead to the problem of "prop drilling."

Imagine a theme setting (light/dark mode) that needs to be accessible by components at various depths. Without a global state solution, you'd have to pass the theme prop through every component in the hierarchy, even if those components don't directly use the theme themselves.

The React Context API provides a way to pass data through the component tree without having to pass props down manually at every level. It's designed to share "global" data, such as the current authenticated user, theme (light/dark mode), or preferred language.

React Context API vs Prop Drilling

Key concepts of the Context API:

Example: Let's create a simple theme switcher using Context API.

// src/context/ThemeContext.jsx
import { createContext, useState } from 'react';

// 1. Create the Context object
const ThemeContext = createContext();

// 2. Create a Provider component
const ThemeProvider = ({ children }) => {
    const [theme, setTheme] = useState('light'); // Global theme state

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

    // The value prop contains the data/functions to be shared
    const contextValue = {
    theme,
    toggleTheme,
    };

    return (
    <ThemeContext.Provider value={contextValue}>
        {children} {/* Renders all child components wrapped by this Provider */}
    </ThemeContext.Provider>
    );
};

export { ThemeContext, ThemeProvider };

// src/components/ThemeSwitcher.jsx
import { useContext } from 'react';
import { ThemeContext } from '../context/ThemeContext'; // Import the Context

function ThemeSwitcher() {
    // Use useContext to access the value provided by ThemeContext.Provider
    const { theme, toggleTheme } = useContext(ThemeContext);

    return (
    <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
    </button>
    );
}

export default ThemeSwitcher;

// src/components/Content.jsx
import { useContext } from 'react';
import { ThemeContext } from '../context/ThemeContext';

function Content() {
    const { theme } = useContext(ThemeContext);

    return (
    <div
      style={{
        backgroundColor: theme === "light" ? "#fff" : "#333",
        color: theme === "light" ? "#333" : "#fff",
        padding: "20px",
      }}
    >
        <p>This content's background and text color change with the theme.</p>
        <p>Current theme: <strong>{theme}</strong></p>
    </div>
    );
}

export default Content;

// src/App.jsx
import { ThemeProvider } from './context/ThemeContext'; // Import the Provider
import ThemeSwitcher from './components/ThemeSwitcher';
import Content from './components/Content';

function App() {
    return (
    <ThemeProvider>
        {/* Wrap the entire app or relevant parts with the Provider */}
        <div>
        <h1>My Themed App</h1>
        <ThemeSwitcher />
        <Content />
        </div>
    </ThemeProvider>
    );
}

export default App;

Context is ideal for "global" data like user authentication status, theme settings, or locale, that rarely changes and is needed by many components. For more complex state management with frequent updates, libraries like Redux or Zustand might be more suitable.


Performance Optimization with useMemo and useCallback

React is generally fast, but unnecessary re-renders can impact performance, especially in large applications. Hooks like useMemo and useCallback help "memoize" (cache) values and functions to prevent redundant computations and re-renders.

By default, when a parent component re-renders, all its child components also re-render, even if their props haven't explicitly changed. This is often fine, but if a child component performs expensive calculations or receives complex props (like functions or objects that are re-created on every parent render), it can lead to performance bottlenecks.

useMemo: Memoizing Values

useMemo is a React Hook that lets you cache the result of a calculation between re-renders. It takes two arguments: a function that computes a value and a dependency array. React will only re-run your memoized function when one of the dependencies in the array has changed.

// src/components/MemoExample.jsx
import { useState, useMemo } from 'react';

// An expensive calculation function
const calculateExpensiveValue = (num) => {
  console.log('Calculating expensive value...');
  let result = 0;
  for (let i = 0; i < 1000000000; i++) { // Simulate heavy computation
    result += i;
  }
  return result + num;
};

function MemoExample() {
  const [count, setCount] = useState(0);
  const [inputNum, setInputNum] = useState(1);

  // useMemo will only re-run calculateExpensiveValue if inputNum changes
  const memoizedExpensiveValue = useMemo(() => {
    return calculateExpensiveValue(inputNum);
  }, [inputNum]); // Dependency array: re-calculate only when inputNum changes

  return (
    <div>
      <h2>useMemo for Expensive Calculations</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count (No Re-calc)</button>

      <div>
        <label>
          Input Number:
          <input
            type="number"
            value={inputNum}
            onChange={(e) => setInputNum(parseInt(e.target.value) || 0)}
          />
        </label>
      </div>
      <p>Expensive Value: {memoizedExpensiveValue}</p>
    </div>
  );
}

export default MemoExample;

Notice how "Calculating expensive value..." only logs when "Input Number" changes, not when "Count" changes, demonstrating the memoization.

useCallback: Memoizing Functions

useCallback is a React Hook that lets you cache a function definition between re-renders. It takes a function and a dependency array. React will return the memoized version of the callback function only if one of the dependencies has changed.

Use useCallback when passing callback functions as props to optimized child components (e.g., components wrapped in React.memo). Without useCallback, a new function instance would be created on every re-render of the parent, causing the child component to re-render unnecessarily, even if its props logically haven't changed.

// src/components/CallbackExample.jsx
import { useState, useCallback, memo } from 'react';

// A child component that will only re-render if its props change
const ChildComponent = memo(({ onClick }) => {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>Click Child</button>;
});

function CallbackExample() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // This function will only be re-created if 'count' changes
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []); // Empty dependency array: this function is memoized and won't change

  // This function will only be re-created if 'text' changes
  const handleTextChangeMemoized = useCallback((e) => {
    setText(e.target.value);
  }, []); // Empty dependency array: this function is memoized and won't change

  return (
    <div>
      <h2>useCallback for Function Memoization</h2>
      <p>Parent Count: {count}</p>
      <input
        type="text"
        value={text}
        onChange={handleTextChangeMemoized} // Use the memoized handler
        placeholder="Type something..."
      />
      <ChildComponent onClick={handleClick} />
      {/* Pass the memoized callback */}
      <p>Text: {text}</p>
    </div>
  );
}

export default CallbackExample;

It's important to note that useMemo and useCallback are performance optimizations, not guarantees. Use them judiciously, as memoization itself comes with a small overhead. They are most beneficial for components that re-render frequently with complex props or expensive calculations, or when optimizing interactions between parent and child components.


Basic Data Fetching

Most modern web applications interact with backend APIs to retrieve or send data. In React, you typically perform data fetching within the useEffect Hook, ensuring that the fetching occurs at the appropriate time in the component's lifecycle and doesn't cause unintended side effects during rendering.

Common ways to fetch data in JavaScript/React:

// src/components/PostsList.jsx
import { useState, useEffect } from 'react';

function PostsList() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchPosts = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts');
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        setPosts(data);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchPosts();
  }, []);

  if (loading) {
    return <div>Loading posts...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      <h2>Posts</h2>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <h3>{post.title}</h3>
            <p>{post.body}</p>
          </li>
        ))}</ul>
    </div>
  );
}

export default PostsList;

Explanation of the Data Fetching Example: