← Back to the Build Your Homepage series
⚛️
EPISODE 05
Extracting logic · naming rules · composition · memo · useMemo · useCallback · profiler · file-based routing · layouts · loading · error · dynamic routes · When to reach for Zustand / Redux / Jotai · TanStack Query for server state

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.

custom hooksreuseuseLocalStorageuseFetchperformancememouseMemouseCallbackProfilerNext.jsApp Routerlayoutsloadingerrorstate managementZustandReduxTanStack Query
Duration
⏱ About 2 hours + About 2 hours + About 2.5 hours + About 2 hours
Level
📊 Intermediate
Prerequisite
🎯 react-02
OUTCOME
Build at least three reusable hooks used across components / Find and fix slow renders with the Profiler / A multi-page Next.js app with nested layouts, dynamic routes, and loading/error UI / Pick the right state tool for each use case and ship one with Zustand or TanStack Query

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

tsx
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

tsx
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)

tsx
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

tsx
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

tsx
// 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

text
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/:slug

2. Layouts

tsx
// 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

tsx
// 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

tsx
// 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)

tsx
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

tsx
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.

Example code / lecture materials

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