Advanced React Concepts

Advanced React Concepts

Data Fetching, Styling, State Management & Performance

CSCI 4513

Week 12, Lecture 21

Today's Journey

Norse Mythology Connection

Yggdrasil - The World Tree

Yggdrasil connects all nine realms, with roots reaching deep wells of knowledge and branches stretching to the heavens, just as these advanced React concepts connect and support every part of your application.

The Three Roots: Yggdrasil has three massive roots that draw from three wells - UrΓ°arbrunnr (Well of Fate), MΓ­misbrunnr (MΓ­mir's Well of Wisdom), and Hvergelmir (The Roaring Kettle). Each provides different sustenance to the tree, just as fetching data, managing state, and optimizing performance each sustain different aspects of your React applications.

Part 1: Fetching Data in React

Connecting to External Sources

Why fetch data?

  • Most apps need data from APIs
  • Content changes frequently
  • Data is too large to bundle
  • Real-time updates needed

Basic Fetch Request

// Vanilla JavaScript fetch
const image = document.querySelector("img");

fetch("https://picsum.photos/v2/list")
  .then((response) => response.json())
  .then((response) => {
    image.src = response[0].download_url;
  })
  .catch((error) => console.error(error));

This works in regular JavaScript, but React needs a different approach!

Fetch in React Components

import { useEffect, useState } from "react";

const Image = () => {
  const [imageURL, setImageURL] = useState(null);

  useEffect(() => {
    fetch("https://picsum.photos/v2/list")
      .then((response) => response.json())
      .then((response) => setImageURL(response[0].download_url))
      .catch((error) => console.error(error));
  }, []);

  return (
    imageURL && (
      <>
        

An image

placeholder ) ); };

Why useEffect for Fetching?

Handling Errors

const [imageURL, setImageURL] = useState(null);
const [error, setError] = useState(null);

useEffect(() => {
  fetch("https://picsum.photos/v2/list")
    .then((response) => {
      if (response.status >= 400) {
        throw new Error("server error");
      }
      return response.json();
    })
    .then((response) => setImageURL(response[0].download_url))
    .catch((error) => setError(error));
}, []);

if (error) return 

A network error was encountered

;

Adding Loading State

const [imageURL, setImageURL] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
  fetch("https://picsum.photos/v2/list")
    .then((response) => {
      if (response.status >= 400) throw new Error("server error");
      return response.json();
    })
    .then((response) => setImageURL(response[0].download_url))
    .catch((error) => setError(error))
    .finally(() => setLoading(false));
}, []);

if (loading) return 

Loading...

; if (error) return

A network error was encountered

;

Custom Hooks for Fetching

const useImageURL = () => {
  const [imageURL, setImageURL] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("https://picsum.photos/v2/list")
      .then((response) => {
        if (response.status >= 400) throw new Error("server error");
        return response.json();
      })
      .then((response) => setImageURL(response[0].download_url))
      .catch((error) => setError(error))
      .finally(() => setLoading(false));
  }, []);

  return { imageURL, error, loading };
};

Using Custom Hooks

const Image = () => {
  const { imageURL, error, loading } = useImageURL();

  if (loading) return 

Loading...

; if (error) return

A network error was encountered

; return ( <>

An image

placeholder ); };

Benefits: Reusable, testable, organized!

Avoiding Request Waterfalls

⚠️ Common Mistake

Making requests in child components that depend on parent requests creates waterfalls - each request waits for the previous one

Solution: Lift requests up to common ancestor and pass data as props

Part 2: Styling React Applications

Making Things Beautiful

Challenge: CSS is global by default

As apps grow, class names can conflict!

CSS Modules

/* Button.module.css */
.button {
  background-color: blue;
  color: white;
  padding: 10px 20px;
}

.primary {
  background-color: green;
}
// Button.jsx
import styles from './Button.module.css';

function Button({ primary, children }) {
  return (
    
  );
}

CSS Modules Benefits

Recommended for this course!

CSS-in-JS

import styled from 'styled-components';

const Button = styled.button`
  background-color: ${props => props.primary ? 'green' : 'blue'};
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;

  &:hover {
    opacity: 0.8;
  }
`;

// Usage

CSS Utility Frameworks

// Tailwind CSS example
function Button({ primary, children }) {
  return (
    
  );
}

For learning: Avoid frameworks, build from scratch!

Component Libraries

Pre-built components with styling and behavior:

  • Material UI (MUI)
  • Radix UI
  • Chakra UI
  • Ant Design

For this course: Build your own components to learn!

Part 3: Context API

Global State Management

Problem: Prop drilling

Passing props through many levels of components gets repetitive and hard to manage

The Prop Drilling Problem

function App() {
  const [user, setUser] = useState({ name: "Alice" });

  return 
; } function Header({ user }) { return

user passed through Header and Nav just to reach UserMenu!

Creating a Context

import { createContext } from "react";

// Create context with default value
const UserContext = createContext({
  user: null,
  setUser: () => {},
});

export default UserContext;

Default value: Used when component isn't wrapped in Provider (also helps with IDE autocomplete!)

Providing Context

import { useState } from "react";
import UserContext from "./UserContext";

function App() {
  const [user, setUser] = useState({ name: "Alice" });

  return (
    
      
); }

Any component inside UserContext can access the value!

Consuming Context

import { useContext } from "react";
import UserContext from "./UserContext";

function UserMenu() {
  const { user, setUser } = useContext(UserContext);

  return (
    

Welcome, {user.name}!

); }

No prop drilling needed! Direct access to context value

Context API: Complete Example

// ShopContext.jsx
export const ShopContext = createContext({
  products: [],
  cartItems: [],
  addToCart: () => {},
});

// App.jsx
function App() {
  const [cartItems, setCartItems] = useState([]);
  const products = useProducts(); // custom hook
  const addToCart = (product) => {
    setCartItems([...cartItems, product]);
  };

  return (
    
      
); }

Context Drawbacks

⚠️ Performance Issues

When context value changes, ALL consumers re-render, even if they don't use the changed data

⚠️ Can Make Code Harder to Follow

Data can come from anywhere - be organized!

Context Best Practices

Part 4: useReducer

Managing Complex State Logic

When to use reducers:

  • Component has complex state logic
  • State updates depend on previous state
  • Want to separate state logic from component

What is a Reducer?

A pure function that takes:

  • state - current state
  • action - object describing what happened

Returns: New state

function reducer(state, action) {
  switch (action.type) {
    case "incremented":
      return { count: state.count + 1 };
    case "decremented":
      return { count: state.count - 1 };
    default:
      throw new Error("Unknown action: " + action.type);
  }
}

Using useReducer

import { useReducer } from "react";

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    

Count: {state.count}

); }

Actions with Payload

function reducer(state, action) {
  switch (action.type) {
    case "set_count":
      return { count: action.value };
    case "incremented":
      return { count: state.count + 1 };
    default:
      throw new Error("Unknown action: " + action.type);
  }
}

// Usage
dispatch({ type: "set_count", value: 10 });

useState vs useReducer

Use useState when:

  • Simple state (single value)
  • Independent state updates
  • State doesn't depend on previous state

Use useReducer when:

  • Complex state (multiple sub-values)
  • State transitions are complex
  • Next state depends on previous state
  • Want to test state logic separately

Part 5: Refs & Memoization

Performance Optimization

Topics:

  • useRef - Accessing DOM & persisting values
  • useMemo - Memoizing expensive calculations
  • useCallback - Memoizing functions

The useRef Hook

Use cases:

  • Access DOM elements directly
  • Store mutable values that don't trigger re-renders
  • Persist values across renders
import { useRef, useEffect } from "react";

function TextInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return ;
}

useRef vs useState

useState:

  • Triggers re-render when updated
  • Immutable updates (create new value)
  • For data that affects UI

useRef:

  • Does NOT trigger re-render
  • Mutable (can change .current)
  • For data that doesn't affect UI

The useMemo Hook

Memoization: Caching expensive calculations

import { useMemo } from "react";

function ProductList({ products }) {
  const totalPrice = useMemo(() => {
    return products.reduce(
      (total, product) => total + product.price * product.quantity,
      0
    );
  }, [products]);

  return 

Total: ${totalPrice}

; }

When to Use useMemo

βœ… Use useMemo for:

  • Expensive calculations
  • Filtering/sorting large arrays
  • Creating objects/arrays for props

❌ Don't use for:

  • Simple calculations
  • Premature optimization

"Premature optimization is the root of all evil"

The useCallback Hook

import { useCallback } from "react";

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

  // Without useCallback, this function is recreated on every render
  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return ;
}

useCallback memoizes functions (just like useMemo, but specifically for functions)

useMemo vs useCallback

// useMemo - memoizes the RESULT of a function
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

// useCallback - memoizes the FUNCTION itself
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b]
);

// These are equivalent:
useCallback(fn, deps)
useMemo(() => fn, deps)

Preventing Unnecessary Re-renders

import { memo } from "react";

// Wrap component with memo
const ExpensiveChild = memo(({ onIncrement }) => {
  console.log("Child rendered");
  return ;
});

// Now child only re-renders if props actually change!

Note: memo + useCallback work together for optimization

Referential Equality

// Every render creates a new function
function Parent() {
  const handleClick = () => console.log("clicked");
  return ;
}

// Even though the function does the same thing,
// it's a NEW reference each time!
// {} !== {} // true (different objects)
// () => {} !== () => {} // true (different functions)

This is why useCallback is useful with memo!

Performance Best Practices

Review: Fetching Data

Review: Styling

Review: Context API

Review: useReducer

Review: Refs & Memoization

Today's Takeaways

You Can Now:

πŸ“… Next Class: Databases & SQL

πŸ“ Homework: Shopping Cart Project