Custom Hooks · Performance · Next.js App Router · Global State Management
Move repeated stateful logic into reusable hook functions. Learn the naming rules, common patterns (useToggle, useFetch, useLocalStorage), and composition. Stop unnecessary re-renders. Learn React.memo, useMemo, useCallback, and how to read the React Profiler — but only optimize when you have actually measured a problem. Master Next.js App Router: folders become routes, layouts wrap pages, dynamic segments handle slugs, and special files give you loading and error UI for free. Decide between local state, Context, and a state library. Compare Zustand, Redux Toolkit, and Jotai briefly, and use TanStack Query for server state.
What you'll learn
- 1Name custom hooks with the use prefix
- 2Return a tuple or object from a hook
- 3Compose hooks within hooks
- 4Write a useLocalStorage and useFetch
- 5Recognize when React.memo helps
- 6Use useMemo for expensive computations
- 7Use useCallback for stable function references
- 8Read the React Profiler flame chart
- 9Map folders to routes (page.tsx, layout.tsx)
- 10Nest layouts to share UI
- 11Use dynamic segments ([slug]) and generateStaticParams
- 12Add loading.tsx and error.tsx for built-in states
- 13Distinguish UI state from server state
- 14Compare Zustand, Redux Toolkit, Jotai at a high level
- 15Use TanStack Query for fetching, caching, revalidation
- 16Know when Context is enough
Custom Hooks
Move repeated stateful logic into reusable hook functions. Learn the naming rules, common patterns (useToggle, useFetch, useLocalStorage), and composition.
1. useToggle
function useToggle(initial = false): [boolean, () => void] {
const [on, setOn] = useState(initial);
const toggle = useCallback(() => setOn(v => !v), []);
return [on, toggle];
}
// Use
const [isOpen, toggleOpen] = useToggle(false);2. useLocalStorage
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
try {
const raw = localStorage.getItem(key);
return raw !== null ? JSON.parse(raw) as T : initial;
} catch { return initial; }
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}3. useFetch (Simple Version)
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
setLoading(true); setError(null);
fetch(url)
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
.then(d => { if (!cancelled) setData(d as T); })
.catch(e => { if (!cancelled) setError(e); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [url]);
return { data, error, loading };
}For production apps, prefer TanStack Query or SWR — they handle caching, retries, and revalidation for you.
4. Naming Rules
- Must start with "use" — that's how React detects a hook
- Hooks can only call other hooks at the top level (no conditional calls)
- Hooks can only be called from React function components or other hooks
Performance
Stop unnecessary re-renders. Learn React.memo, useMemo, useCallback, and how to read the React Profiler — but only optimize when you have actually measured a problem.
1. The Default Render Cost is Cheap
Premature optimization is the root of all evil. Render performance issues are rare; measure first.
2. React.memo
const Heavy = React.memo(function Heavy({ value }: { value: number }) {
// expensive render
return <div>{value}</div>;
});
// Only re-renders when value (a primitive) changes.3. useMemo & useCallback
// Cache an expensive value
const sorted = useMemo(() => items.slice().sort(compare), [items]);
// Cache a function so child memo doesn't re-render
const handleClick = useCallback(() => doThing(id), [id]);If your child is wrapped in React.memo and receives functions/objects as props, useCallback/useMemo keep those references stable.
4. React Profiler
- DevTools → Profiler tab → record an interaction
- Look at the flame chart for components rendering longer than ~16ms
- Inspect why a component rendered (Why did this render? section)
Next.js App Router
Master Next.js App Router: folders become routes, layouts wrap pages, dynamic segments handle slugs, and special files give you loading and error UI for free.
1. Folder Routing
app/
├── layout.tsx # wraps everything
├── page.tsx # /
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ ├── layout.tsx # wraps /blog and nested
│ └── [slug]/
│ └── page.tsx # /blog/:slug2. Layouts
// app/layout.tsx (root)
import "./globals.css";
import Link from "next/link";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<nav><Link href="/">Home</Link> · <Link href="/about">About</Link></nav>
<main>{children}</main>
</body>
</html>
);
}Nested layouts inherit from the parent — you can have section-specific headers, sidebars, etc.
3. Dynamic Routes & Static Params
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(p => ({ slug: p.slug }));
}
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return <article><h1>{post.title}</h1>{post.body}</article>;
}4. Special Files: loading & error
// app/blog/loading.tsx
export default function Loading() { return <p>Loading posts...</p>; }
// app/blog/error.tsx (must be a client component)
"use client";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}Global State Management
Decide between local state, Context, and a state library. Compare Zustand, Redux Toolkit, and Jotai briefly, and use TanStack Query for server state.
1. Decision Tree
- Local to one component? → useState/useReducer
- Shared across a small subtree? → Context
- Shared widely, changes often? → state library (Zustand recommended for new projects)
- Server data (caching/revalidation)? → TanStack Query or SWR
2. Zustand (Tiny, Recommended)
import { create } from "zustand";
type Store = {
count: number;
increment: () => void;
reset: () => void;
};
export const useCounter = create<Store>((set) => ({
count: 0,
increment: () => set(s => ({ count: s.count + 1 })),
reset: () => set({ count: 0 }),
}));
// Anywhere in the tree
function Counter() {
const { count, increment } = useCounter();
return <button onClick={increment}>Count: {count}</button>;
}3. TanStack Query for Server State
import { useQuery } from "@tanstack/react-query";
function Users() {
const { data, isLoading, error } = useQuery({
queryKey: ["users"],
queryFn: () => fetch("/api/users").then(r => r.json()),
staleTime: 60_000,
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}Query handles caching, dedupe, refetch on focus, background revalidation, and pagination. Stop building this yourself.
4. Redux Toolkit (When You Need It)
Redux Toolkit is fine for large apps with complex actions, undo, time-travel debugging. For new small/medium apps, Zustand or Context is usually enough.
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗Comments 0
No comments yet. Be the first.
Only members can comment.
Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy