Hooks Mastery: The Engine of Interactivity
DraftURL: https://ruvebal.github.io/web-atelier-udit/lessons/en/react/react-hooks/
π Table of Contents
- π― Sprint Goal
- π Position in Journey
- π§ Learning Objectives
- ποΈ What Weβll Build This Sprint
- π§ Integration Points
- π Methodology: Atelier Practice
- π‘ Production-Ready Custom Hooks
- π― Critical Questions: Atelier Methodology
- π Note: Memoization in React vs Other Environments
- π Sprint Deliverables
- π Lesson Navigation
- π Key Concepts Preview
βA hook is a portal between the declarative world of React and the imperative world of effects.β
π― Sprint Goal
By the end of this sprint: Transform your static components into living, breathing interactive elements with state, effects, and reusable logic patterns.
π Position in Journey
| Sprint | Focus | Your App Grows |
|---|---|---|
| 5. Fundamentals | Components, JSX, Props | Component library skeleton |
| β 6. Hooks | State & effects | Interactive components |
| 7. Architecture | Global state | Connected features |
| 8. Routing | Navigation | Multi-page structure |
π§ Learning Objectives
By the end of this lesson, you will:
- Use
useStatefor local component state - Master
useEffectfor side effects and cleanup - Apply
useReffor DOM access and mutable values - Optimize with
useMemoanduseCallback - Extract reusable logic into custom hooks
- Avoid common pitfalls (stale closures, infinite loops)
ποΈ What Weβll Build This Sprint
Custom Hooks for Your App
// Hooks you'll create this sprint:
useFetch(url) // β { data, loading, error }
useLocalStorage(key) // β [value, setValue]
useDebounce(value, delay) // β debouncedValue
useToggle(initial) // β [state, toggle, setTrue, setFalse]
useForm(initialValues) // β { values, handleChange, reset }
These hooks will power your entire application.
π§ Integration Points
| Data Source | Hook Usage |
|---|---|
| Laravel API | useFetch for GET requests, custom useMutation for POST |
| Hygraph CMS | useQuery pattern for GraphQL (Apollo or custom) |
| Local Storage | useLocalStorage for persistence (theme, preferences) |
Preview: API Integration Pattern
// This sprint's hook...
const { data, loading, error } = useFetch('/api/products');
// ...prepares you for next sprint's React Query
const { data, isLoading, error } = useQuery(['products'], fetchProducts);
π Methodology: Atelier Practice
The Sprint Rhythm
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DAY 1: Core Hooks Deep Dive β
β β’ useState patterns: primitives, objects, arrays β
β β’ useEffect lifecycle: mount, update, unmount β
β β’ Live debugging: React DevTools, console patterns β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β DAY 2: Custom Hooks Workshop β
β β’ Build `useFetch` together step by step β
β β’ Teams create 2-3 hooks for their specific app β
β β’ AI Practice: Generate hook tests with Copilot β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β DAY 3: Integration & Polish β
β β’ Wire hooks to your components from Sprint 5 β
β β’ Handle loading/error states in UI β
β β’ Peer review: Are hooks single-responsibility? β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
AI-Assisted Development Protocol
Concrete AI Prompts for Hooks
β
GOOD PROMPT:
"Create a custom useFetch hook that:
1. Accepts a URL and optional fetch options
2. Returns { data, loading, error, refetch }
3. Handles race conditions (ignore stale requests)
4. Cleans up on unmount
5. Returns an object with data, loading, error and refetch"
β BAD PROMPT:
"Make a fetch hook"
β
VALIDATION PROMPT:
"Review this useEffect for:
1. Missing dependencies that could cause bugs
2. Memory leaks (missing cleanup)
3. Infinite loop risks
4. Race conditions in async operations"
π WHEN NOT TO USE AI:
- Debugging stale closure issues (requires deep understanding)
- Deciding when to use useCallback vs useMemo (performance profiling needed)
- Understanding why useEffect runs twice in dev mode (React fundamentals)
| Task | AI Role | Your Role |
|---|---|---|
| Debug useEffect dependency array | Explain the warning | Understand why |
| Generate custom hook skeleton | Scaffold basic structure | Add error handling |
| Write hook tests | Draft test cases | Verify edge cases |
| Optimize re-renders | Suggest memoization | Profile before/after |
π‘ Production-Ready Custom Hooks
Example 1: useFetch Hook (Best Practices)
// hooks/useFetch.js
import { useState, useEffect, useRef } from 'react';
export function useFetch(url, options) {
const [state, setState] = useState({
data: null,
loading: true,
error: null,
});
// Track latest request to handle race conditions
const abortControllerRef = useRef(null);
const fetchData = async () => {
// Cancel previous request if still pending
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// Create new abort controller for this request
const abortController = new AbortController();
abortControllerRef.current = abortController;
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const response = await fetch(url, {
...options,
signal: abortController.signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Only update if this request wasn't aborted
if (!abortController.signal.aborted) {
setState({ data, loading: false, error: null });
}
} catch (error) {
// Ignore abort errors
if (error instanceof Error && error.name === 'AbortError') {
return;
}
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
};
useEffect(() => {
fetchData();
// Cleanup: abort request on unmount
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [url]); // Re-fetch when URL changes
return { ...state, refetch: fetchData };
}
Usage:
function ProductList() {
const { data, loading, error, refetch } = useFetch('/api/products');
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} onRetry={refetch} />;
if (!data) return null;
return (
<div>
<button onClick={refetch}>Refresh</button>
{data.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
Example 2: useLocalStorage Hook
// hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
export function useLocalStorage(key, initialValue) {
// Get from localStorage or use initial value
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
// Return a wrapped version of useState's setter that persists to localStorage
const setValue = (value) => {
try {
// Allow value to be a function (same API as useState)
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue];
}
Usage:
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
}
Example 3: useDebounce Hook
// hooks/useDebounce.js
import { useState, useEffect } from 'react';
export function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Set up the timeout
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cleanup: cancel timeout if value changes before delay
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
Usage:
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
// This effect only runs when debounced value changes
useEffect(() => {
if (debouncedSearchTerm) {
// Make API call
searchAPI(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
π― Critical Questions: Atelier Methodology
On Hook Design
π Question 1: The Dependency Array Dilemma
Your
useEffecthas 5 dependencies. ESLint warns about missing ones. Adding them causes infinite loops. Removing them causes stale data.Reflect:
- Is this a sign your effect is doing too much?
- When should you split one effect into multiple effects?
- How do you decide between
useCallbackand accepting the re-run?- What does this reveal about Reactβs mental model?
π Question 2: Custom Hook Abstraction
Youβve extracted
useFetchbut now every component needs slightly different behavior:
- Component A needs caching
- Component B needs retry logic
- Component C needs request cancellation
Reflect:
- Do you add all features to one hook (bloat)?
- Do you create 3 separate hooks (duplication)?
- Do you use composition (hooks calling hooks)?
- When does a custom hook become a library?
π Question 3: The useEffect Escape Hatch
React docs say: βYou might not need an effect.β But your AI keeps suggesting useEffect for everything.
Reflect:
- When is useEffect the wrong tool?
- What can be done during render instead?
- How do you recognize βderived stateβ vs βsynchronized stateβ?
- Why does React discourage effects?
On AI-Assisted Development
π Question 4: The Stale Closure Trap
AI generated this code:
useEffect(() => { const interval = setInterval(() => { setCount(count + 1); // BUG: count is stale! }, 1000); return () => clearInterval(interval); }, []);It looks correct but doesnβt work. AI didnβt catch it.
Reflect:
- Why didnβt AI see this bug?
- How do you develop βclosure intuitionβ?
- Whatβs the fix? (Hint: functional update)
- Can you trust AI for async/closure code?
π Question 5: Performance Premature Optimization
AI suggests wrapping everything in
useMemoanduseCallback. Your app has 50 memoized values but no measured performance problem.Reflect:
- Is this optimization or obfuscation?
- How do you measure if memoization helped?
- Whatβs the cost of memoization itself?
- When should you profile BEFORE optimizing?
On Atelier Collaboration
π Question 6: Hook Patterns Divergence
Your team has 3 different fetch hooks:
useFetch(yours)useAPI(teammate A)useData(teammate B)All do similar things differently.
Reflect:
- How do you consolidate without hurting feelings?
- What makes a hook pattern βbetterβ?
- Should the team standardize, or is diversity okay?
- How do real teams handle this?
π Question 7: The Learning Curve
A teammate asks: βWhy is my useEffect running twice?β You know itβs React 18 Strict Mode, but theyβre frustrated.
Reflect:
- How do you explain without sounding condescending?
- Whatβs the pedagogical value of this React behavior?
- Should beginners learn hooks or start with class components?
- How do you teach βwhyβ not just βhowβ?
π Note: Memoization in React vs Other Environments
When you work with useMemo, useCallback, and React.memo, itβs natural to ask: why does React make us care so much about references? This note puts Reactβs design in context.
Why React Relies on Reference Equality
In JavaScript, equality is by reference (===). On every re-render, a function or array created during render is a new value in memoryβsame behavior, different reference. To React (and to React.memo) that means βprops changed,β so the child re-renders. Thatβs why we use useCallback and useMemo: not to stop the parent from re-rendering, but to keep the same reference when the logical value hasnβt changed, so memoized children donβt receive βnewβ props and skip unnecessary work.
How Other Languages and Frameworks Handle It
| Approach | Example | Main idea |
|---|---|---|
| Structural equality | Clojure, Elm, Rust (with equality traits) | Two values that βlook the sameβ are considered equal even if theyβre different references. The framework can decide whether to recompute without you manually stabilizing references. |
| Compiler or runtime tracks dependencies | Svelte, Vue (ref/computed), SwiftUI |
Svelte analyzes which variables each block uses and generates code that only re-runs when those changeβno useMemo or useCallback. Vue and SwiftUI encapsulate βwhat this depends onβ in a similar way. |
| Immutable data by default | Elm, ClojureScript | Data isnβt mutated; equality is usually structural. βDid it change?β is answered by value, not reference, so the βsame function, new referenceβ issue doesnβt arise in the same way. |
Takeaway
Itβs not that βJavaScript is just like thatβ: React chose an explicit model where you control identity (references) and when to optimize. That makes the model very teachable and flexible, but it also forces you to think about references and to use useCallback/useMemo/memo when you want to avoid re-renders or redundant work. In other ecosystems, that concern is often hidden behind structural equality or a compiler/runtime that infers dependencies. Knowing both perspectives helps explain why memoization is part of Reactβs design, not a quirk of the language.
π Sprint Deliverables
- 3+ custom hooks (
useFetch,useLocalStorage,useDebounce) - Interactive feature using useState (e.g., form, toggle)
- Cleanup function in at least one useEffect
- Hook tests for at least
useFetch - Reflection entry addressing at least 3 critical questions above
- Dependency array audit - document why each dependency is needed
- Peer code review focusing on hook patterns and potential bugs
π Lesson Navigation
| Previous | Current | Next |
|---|---|---|
| React Fundamentals | Hooks Mastery | State Architecture |
π Key Concepts Preview
Key concepts you should master after this lesson
- Rules of Hooks: why ordering matters, what βconsistent call sitesβ means
useState: functional updates, derived state avoidanceuseEffect: sync vs effect, dependency reasoning, cleanup disciplineuseRef: stable mutable cell (not βstateβ), escape hatch for integrationuseMemo/useCallback: when and why to memoize; see the note on memoization vs other environments- Custom hooks: composition, configuration vs specialization trade-offs
- Testing: validate behavior, not implementation details
If any of these still feels βmagicalβ, donβt add more hooksβreduce the problem until you can explain it without guessing.
βEvery custom hook is a reusable piece of wisdom, extracted from the chaos of a component.β