CSCI 4513
Week 12, Lecture 21
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.
Why fetch data?
// 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!
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
>
)
);
};
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
;
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
;
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 };
};
const Image = () => {
const { imageURL, error, loading } = useImageURL();
if (loading) return Loading...
;
if (error) return A network error was encountered
;
return (
<>
An image
>
);
};
Benefits: Reusable, testable, organized!
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
Challenge: CSS is global by default
As apps grow, class names can conflict!
/* 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 (
);
}
Recommended for this course!
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
// Tailwind CSS example
function Button({ primary, children }) {
return (
);
}
For learning: Avoid frameworks, build from scratch!
Pre-built components with styling and behavior:
For this course: Build your own components to learn!
Problem: Prop drilling
Passing props through many levels of components gets repetitive and hard to manage
function App() {
const [user, setUser] = useState({ name: "Alice" });
return ;
}
function Header({ user }) {
return ;
}
function Nav({ user }) {
return ;
}
function UserMenu({ user }) {
return Welcome, {user.name}!
;
}
user passed through Header and Nav just to reach UserMenu!
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!)
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!
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
// 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 (
);
}
When context value changes, ALL consumers re-render, even if they don't use the changed data
Data can come from anywhere - be organized!
When to use reducers:
A pure function that takes:
state - current stateaction - object describing what happenedReturns: 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);
}
}
import { useReducer } from "react";
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
Count: {state.count}
);
}
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 });
Topics:
Use cases:
import { useRef, useEffect } from "react";
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return ;
}
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}
;
}
"Premature optimization is the root of all evil"
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 - 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)
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
// 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!
π Next Class: Databases & SQL
π Homework: Shopping Cart Project