Part 2: React Router Deep Dive
Building Modern Single-Page Applications
Setting Up React Router v6
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/blog/:id" element={<BlogPost />} />
</Routes>
</BrowserRouter>
);
}
- BrowserRouter wrapper: Enables routing for entire app
- Routes container: Groups all route definitions
- Element prop with JSX: Direct component rendering
- Path patterns: Static, dynamic, and nested routes
- Automatic exact matching: No ambiguity in route selection
Navigation Components
Link vs NavLink
// Basic navigation
<Link to="/about">About</Link>
// Active styling
<NavLink
to="/about"
className={({ isActive }) =>
isActive ? 'active' : ''
}
>
About
</NavLink>
- Link: Basic navigation without page reload
- NavLink: Adds active state styling
- Function className: Dynamic styling based on route
- No anchor tags: Prevents full page refreshes
Dynamic Routes with useParams
// Route definition
<Route path="/blog/:id" element={<BlogPost />} />
// Component using params
function BlogPost() {
const { id } = useParams();
const [post, setPost] = useState(null);
useEffect(() => {
fetchBlogPost(id).then(setPost);
}, [id]);
return <article>{post?.title}</article>;
}
- :id parameter: Dynamic segment in URL path
- useParams hook: Extracts URL parameters as object
- Effect dependency: Re-fetch when ID changes
- Real-world pattern: Common for detail pages
Programmatic Navigation
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const handleLogin = async (credentials) => {
try {
await login(credentials);
navigate('/dashboard'); // Redirect after login
} catch (error) {
console.error('Login failed');
}
};
return (/* form JSX */);
}
- useNavigate hook: Returns navigation function
- Imperative navigation: Navigate based on logic
- Common patterns: After form submission, authentication
- Replace option: navigate('/path', { replace: true })
- Go back: navigate(-1) for browser back button
Nested Routes & Outlets
// Parent route with nested children
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Overview />} />
<Route path="analytics" element={<Analytics />} />
<Route path="settings" element={<Settings />} />
</Route>
// DashboardLayout component
function DashboardLayout() {
return (
<div className="dashboard">
<Sidebar />
<main>
<Outlet /> {/* Renders nested route */}
</main>
</div>
);
}
- Layout persistence: Sidebar stays while content changes
- Outlet component: Placeholder for nested routes
- Index route: Default child route
- URL structure: /dashboard/analytics
Part 3: useRef Hook Mastery
Direct DOM Access & Value Persistence
Understanding useRef
Core Concept: useRef creates a mutable reference that persists across renders without triggering re-renders
- Two main uses: DOM references and mutable values
- Returns object: { current: value }
- Persists between renders: Unlike regular variables
- No re-render on change: Unlike useState
- Escape hatch: For imperative DOM operations
DOM Element References
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// Focus input on mount
inputRef.current?.focus();
}, []);
const selectAll = () => {
inputRef.current?.select();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={selectAll}>Select All</button>
</div>
);
}
- ref attribute: Connects ref to DOM element
- current property: Actual DOM node
- Imperative methods: focus(), select(), scrollIntoView()
- Null checking: Element might not exist yet
- Common uses: Focus management, animations, measurements
Storing Previous Values
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count; // Store after render
});
const prevCount = prevCountRef.current ?? 0;
return (
<div>
<p>Current: {count}, Previous: {prevCount}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</div>
);
}
- Effect without deps: Runs after every render
- Timing matters: Previous value updated after render
- No infinite loop: Ref changes don't trigger renders
- Use case: Comparing values, animations, undo features
Memory Leak Prevention
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef();
const startTimer = () => {
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(intervalRef.current);
};
useEffect(() => {
return () => clearInterval(intervalRef.current);
}, []);
return (/* timer UI */);
}
- Store interval ID: Need reference for cleanup
- Cleanup function: Prevents memory leaks
- Component unmount: Cleanup runs automatically
- Pattern: Store any cleanup-needed values
Part 4: Performance Optimization
useMemo & useCallback for Efficient React
Understanding Memoization
Definition: Caching results of expensive operations to avoid recalculation
- Problem: Components re-render → functions re-run
- Solution: Cache results when inputs don't change
- Trade-off: Memory for computation time
- When to use: Profiler shows performance issues
- When NOT to use: Premature optimization
useMemo for Expensive Computations
function DataGrid({ data, filter }) {
// Expensive filtering operation
const filteredData = useMemo(() => {
console.log('Filtering data...');
return data.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [data, filter]); // Only re-compute when these change
return (
<table>
{filteredData.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
</tr>
))}
</table>
);
}
- Dependency array: Re-compute only when deps change
- Skips computation: Returns cached value if deps same
- Good for: Complex calculations, data transformations
- Not for: Simple operations, reference equality
- Debug tip: Console.log shows when it runs
useCallback for Function Memoization
function TodoList({ todos }) {
const [filter, setFilter] = useState('all');
// Memoized callback
const deleteTodo = useCallback((id) => {
console.log('Creating delete function');
setTodos(prev => prev.filter(t => t.id !== id));
}, []); // Empty deps - function never changes
return (
<div>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={deleteTodo} // Same reference
/>
))}
</div>
);
}
- Problem solved: Prevents child re-renders
- Reference equality: Same function object
- React.memo benefit: Child skips render if props same
- Event handlers: Common use case
- Dependencies: Include all used values
When to Optimize
✅ Good Uses
- Heavy computations (sorting, filtering large arrays)
- Complex calculations (cryptography, statistics)
- Preventing child re-renders
- Referential equality needs
❌ Avoid When
- Simple operations
- Primitive dependencies
- Infrequent re-renders
- No measured performance issue
Remember: Wrong memoization can make performance worse by adding overhead
Part 5: Custom Hooks
Building Reusable Logic Patterns
Why Custom Hooks?
- Code reuse: Share stateful logic between components
- Separation of concerns: Extract complex logic
- Testing: Easier to test isolated logic
- Composition: Build complex hooks from simple ones
- Convention: Must start with "use"
Pattern: Custom hooks are JavaScript functions that use other hooks
useFetch Custom Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error('Failed');
const json = await response.json();
setData(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
- Encapsulates: Loading states, error handling
- Returns object: Multiple values for flexibility
- Reusable: Any component can fetch data
- Dependencies: Re-fetches when URL changes
useLocalStorage Hook
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('LocalStorage error:', error);
}
};
return [storedValue, setValue];
}
- Persistent state: Survives page refreshes
- API like useState: Familiar interface
- Error handling: Graceful fallbacks
- Use cases: User preferences, draft data
Composing Custom Hooks
// Combine multiple hooks
function useAuthenticatedFetch(url) {
const { token } = useAuth(); // Another custom hook
const { data, loading, error } = useFetch(
token ? `${url}?token=${token}` : null
);
return {
data,
loading,
error: error || (!token && 'Not authenticated')
};
}
// Usage in component
function UserProfile() {
const { data: user, loading } = useAuthenticatedFetch('/api/user');
if (loading) return <Spinner />;
return <Profile user={user} />;
}
- Hook composition: Build complex from simple
- Business logic: Authentication + data fetching
- Clean components: Logic extracted to hooks
- Testability: Mock individual hooks
Custom Hook Best Practices
Rules: Follow the Rules of Hooks + naming convention
- Naming: Always prefix with "use"
- Pure functions: No side effects outside React
- Return consistently: Same shape always
- Document well: Clear API and examples
- Test thoroughly: Use @testing-library/react-hooks
- Keep focused: Single responsibility