Building Dynamic React Applications with Advanced Patterns
From Chapter 6: Side effects with useEffect
"When you click on one of these, we want it to show in this form... that's called an effect or a side effect"
From Chapter 6: Better state management
"Context provides a way to pass data through the component tree without having to pass props down manually at every level"
// From Chapter 7 prep work
const fetchFeedback = async () => {
const response = await fetch('http://localhost:5000/feedback');
const data = await response.json();
setFeedback(data);
setIsLoading(false);
};
import { useEffect, useState } from 'react';
function StudentRoster() {
const [students, setStudents] = useState([]);
useEffect(() => {
// 1. Effect function - runs after render
console.log('Effect running!');
// 2. Side effect happens here
fetchStudents();
// 3. Cleanup function (optional)
return () => {
console.log('Cleaning up!');
};
}, []); // 4. Dependency array
}
function GradeTracker({ courseId, studentId }) {
const [grades, setGrades] = useState([]);
// Runs once on mount
useEffect(() => {
loadInitialData();
}, []);
// Runs when courseId changes
useEffect(() => {
fetchCourseGrades(courseId);
}, [courseId]);
// Runs when either changes
useEffect(() => {
fetchStudentGrades(courseId, studentId);
}, [courseId, studentId]);
}
function LiveScoreboard({ gameId }) {
const [scores, setScores] = useState({});
useEffect(() => {
// Set up subscription
const subscription = subscribeToGame(gameId, (newScores) => {
setScores(newScores);
});
// Cleanup function - CRITICAL!
return () => {
subscription.unsubscribe();
};
}, [gameId]);
}
// DON'T DO THIS!
useEffect(() => {
setCount(count + 1); // Causes re-render, triggers effect again
}); // No dependency array!
useEffect(() => {
setCount(c => c + 1); // Functional update
}, []); // Run once
function CourseList() {
const [courses, setCourses] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchCourses() {
try {
const response = await fetch('/api/courses');
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
if (!cancelled) {
setCourses(data);
setLoading(false);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
setLoading(false);
}
}
}
fetchCourses();
return () => { cancelled = true; };
}, []);
}
// StudentContext.js
import { createContext, useState, useContext } from 'react';
// 1. Create the context
const StudentContext = createContext();
// 2. Create provider component
export function StudentProvider({ children }) {
const [students, setStudents] = useState([]);
const [selectedStudent, setSelectedStudent] = useState(null);
const addStudent = (student) => {
setStudents(prev => [...prev, { ...student, id: Date.now() }]);
};
const value = {
students,
selectedStudent,
addStudent,
setSelectedStudent
};
return (
<StudentContext.Provider value={value}>
{children}
</StudentContext.Provider>
);
}
// 3. Custom hook for using context
export function useStudents() {
const context = useContext(StudentContext);
if (!context) {
throw new Error('useStudents must be used within StudentProvider');
}
return context;
}
// App.js - Wrap with provider
import { StudentProvider } from './StudentContext';
function App() {
return (
<StudentProvider>
<Header />
<StudentList />
<AddStudentForm />
</StudentProvider>
);
}
// StudentList.js - Consume context
import { useStudents } from './StudentContext';
function StudentList() {
const { students, setSelectedStudent } = useStudents();
return (
<ul>
{students.map(student => (
<li key={student.id} onClick={() => setSelectedStudent(student)}>
{student.name}
</li>
))}
</ul>
);
}
function StudentProvider({ children }) {
const [students, setStudents] = useState([]);
const [loading, setLoading] = useState(true);
// Fetch data when provider mounts
useEffect(() => {
async function loadStudents() {
try {
const response = await fetch('/api/students');
const data = await response.json();
setStudents(data);
} catch (error) {
console.error('Failed to load students:', error);
} finally {
setLoading(false);
}
}
loadStudents();
}, []);
const value = { students, loading };
return (
<StudentContext.Provider value={value}>
{children}
</StudentContext.Provider>
);
}
import { useMemo, useCallback } from 'react';
function OptimizedProvider({ children }) {
const [students, setStudents] = useState([]);
// Memoize functions to prevent recreating
const addStudent = useCallback((student) => {
setStudents(prev => [...prev, student]);
}, []);
// Memoize value object
const value = useMemo(() => ({
students,
addStudent
}), [students, addStudent]);
return (
<StudentContext.Provider value={value}>
{children}
</StudentContext.Provider>
);
}
// Three ways to handle asynchronous code
// 1. Callbacks (old way)
getData((error, data) => {
if (error) console.error(error);
else console.log(data);
});
// 2. Promises with .then()
getData()
.then(data => console.log(data))
.catch(error => console.error(error));
// 3. Async/await (modern way)
async function fetchData() {
try {
const data = await getData();
console.log(data);
} catch (error) {
console.error(error);
}
}
Promise: An object representing the eventual completion or failure of an asynchronous operation
// Creating a promise
const myPromise = new Promise((resolve, reject) => {
// Simulate async operation
setTimeout(() => {
const success = true;
if (success) {
resolve({ data: 'Operation successful!' });
} else {
reject('Something went wrong');
}
}, 1000);
});
// Handling a promise
const fetchStudentData = new Promise((resolve, reject) => {
setTimeout(() => {
const error = false;
if (!error) {
resolve({ name: 'Alice', gpa: 3.8 });
} else {
reject('Failed to fetch student data');
}
}, 1000);
});
// Consuming the promise
fetchStudentData
.then(student => {
console.log('Student:', student);
return student.gpa; // Can return value for next .then()
})
.then(gpa => {
console.log('GPA:', gpa);
})
.catch(error => {
console.error('Error:', error);
})
.finally(() => {
console.log('Promise settled - cleanup complete');
});
// Sequential async operations
fetch('/api/courses')
.then(response => {
if (!response.ok) throw new Error('Network error');
return response.json(); // Returns another promise!
})
.then(courses => {
console.log('Courses:', courses);
// Fetch details for first course
return fetch(`/api/courses/${courses[0].id}`);
})
.then(response => response.json())
.then(courseDetails => {
console.log('Course details:', courseDetails);
})
.catch(error => {
console.error('Any step failed:', error);
});
// Run multiple promises in parallel
const promise1 = fetch('/api/students');
const promise2 = fetch('/api/courses');
const promise3 = fetch('/api/grades');
Promise.all([promise1, promise2, promise3])
.then(responses => {
// All responses arrive - parse them all
return Promise.all(responses.map(r => r.json()));
})
.then(([students, courses, grades]) => {
console.log('Students:', students);
console.log('Courses:', courses);
console.log('Grades:', grades);
// All data available simultaneously!
})
.catch(error => {
// If ANY promise fails, catch fires
console.error('At least one request failed:', error);
});
function getCourseData(id) {
return fetch(`/api/courses/${id}`)
.then(res => res.json())
.then(course => {
console.log(course);
return course;
})
.catch(err => {
console.error(err);
throw err;
});
}
async function getCourseData(id) {
try {
const res = await fetch(`/api/courses/${id}`);
const course = await res.json();
console.log(course);
return course;
} catch (err) {
console.error(err);
throw err;
}
}
// Complete fetch pattern with error handling
async function fetchCourseData(courseId) {
const url = `https://api.university.edu/courses/${courseId}`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
// Check HTTP status
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Parse JSON
const data = await response.json();
return data;
} catch (error) {
// Network error or JSON parse error
console.error('Fetch failed:', error);
throw error; // Re-throw for caller to handle
}
}
// REST API patterns
const API_URL = 'https://api.example.com/students';
// GET - Retrieve data
const getStudents = async () => {
const response = await fetch(API_URL);
return response.json();
};
// POST - Create new
const createStudent = async (student) => {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(student)
});
return response.json();
};
// PUT - Update existing
const updateStudent = async (id, updates) => {
const response = await fetch(`${API_URL}/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
return response.json();
};
// DELETE - Remove
const deleteStudent = async (id) => {
await fetch(`${API_URL}/${id}`, { method: 'DELETE' });
};
function StudentDashboard() {
const [students, setStudents] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchStudents()
.then(data => {
setStudents(data);
setError(null);
})
.catch(err => {
setError(err.message);
setStudents([]);
})
.finally(() => {
setLoading(false);
});
}, []);
if (loading) return <div className="spinner">Loading...</div>;
if (error) return <div className="error">Error: {error}</div>;
if (students.length === 0) return <div>No students found</div>;
return (
<div className="student-grid">
{students.map(student => (
<StudentCard key={student.id} student={student} />
))}
</div>
);
}
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
let cancelled = false;
async function search() {
// Debounce search
await new Promise(resolve => setTimeout(resolve, 300));
if (cancelled) return;
try {
const data = await searchAPI(query);
// Only update if this effect hasn't been cancelled
if (!cancelled) {
setResults(data);
}
} catch (error) {
if (!cancelled) {
console.error('Search failed:', error);
}
}
}
if (query) {
search();
} else {
setResults([]);
}
// Cleanup - cancel previous search
return () => {
cancelled = true;
};
}, [query]);
// LibraryContext.js - Production-ready pattern
import { createContext, useContext, useState, useEffect } from 'react';
const LibraryContext = createContext();
export function LibraryProvider({ children }) {
const [books, setBooks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [filter, setFilter] = useState('all');
// Fetch books on mount
useEffect(() => {
fetchBooks();
}, []);
const fetchBooks = async () => {
try {
const response = await fetch('/api/library/books');
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
setBooks(data);
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const addBook = async (bookData) => {
try {
const response = await fetch('/api/library/books', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(bookData)
});
const newBook = await response.json();
setBooks(prev => [...prev, newBook]);
return newBook;
} catch (err) {
setError('Failed to add book');
throw err;
}
};
const filteredBooks = books.filter(book => {
if (filter === 'all') return true;
if (filter === 'available') return book.status === 'available';
if (filter === 'checked-out') return book.status === 'checked-out';
});
const value = {
books: filteredBooks,
loading,
error,
filter,
setFilter,
addBook,
refreshBooks: fetchBooks
};
return (
<LibraryContext.Provider value={value}>
{children}
</LibraryContext.Provider>
);
}
// Custom hook for data fetching
function useAPI(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error(`Error: ${response.status}`);
const result = await response.json();
if (!cancelled) {
setData(result);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
setData(null);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Using the custom hook
function CourseDetails({ courseId }) {
const { data: course, loading, error } = useAPI(`/api/courses/${courseId}`);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <CourseCard course={course} />;
}
You now have all the patterns needed for building real React applications: