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.

Key concepts of the Context API:
-
createContext
: Creates a Context object. When React renders a component that subscribes to this Context object, it will read the current Context value from the closest matchingProvider
above it in the tree. -
Provider
: A React component that allows consuming components to subscribe to context changes. It accepts avalue
prop to be passed down to all its descendants. You wrap the part of your component tree that needs access to this context with the Provider. -
useContext
: Any component nested within aContext.Provider
can access the context's value using theuseContext
Hook, and subscribe to context changes within a function component.
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:
-
fetch
API: A built-in browser API for making network requests. It returns a Promise, making it ideal for use withasync/await
. -
Axios: A popular third-party library that
provides a more convenient and feature-rich way to make HTTP
requests, offering better error handling, automatic JSON
parsing, and interceptors. (For this basic introduction, we
will focus on
fetch
).
// 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:
-
useState
for Data, Loading, and Error: We use three state variables:posts
to store the fetched data,loading
to indicate if data is currently being fetched, anderror
to hold any error that might occur. -
useEffect
Hook: ThefetchPosts
async function is called insideuseEffect
. The empty dependency array[]
ensures thatfetchPosts
runs only once after the component mounts, similar tocomponentDidMount
in class components. This is ideal for initial data loading. -
fetch
API:-
await fetch(...)
makes the HTTP GET request. -
response.ok
checks if the HTTP status is in the 200-299 range (successful). -
await response.json()
parses the JSON response body into a JavaScript object.
-
-
Error Handling: A
try...catch
block is used to catch any network errors or issues with the response. -
Loading State: The
loading
state is set totrue
before fetching andfalse
after fetching (whether successful or failed). This allows us to display a "Loading..." message to the user, providing immediate feedback and improving the perceived performance of the application. -
Conditional Rendering: The component
conditionally renders a "Loading..." message, an "Error"
message, or the list of posts based on the
loading
anderror
states. This is crucial for providing a robust and user-friendly interface, informing the user about the application's status and any issues that arise.