Part 5: Custom Hooks, Context API, and Routing

Welcome to Part 5 where we’ll elevate your React skills with professional patterns for state management and navigation. These techniques are used in production-grade React applications!

Creating Custom Hooks

Custom hooks let you extract component logic into reusable functions.

useLocalStorage Hook

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const storedValue = localStorage.getItem(key);
    return storedValue ? JSON.parse(storedValue) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// Usage:
function ThemeToggle() {
  const [darkMode, setDarkMode] = useLocalStorage('darkMode', false);
  
  return (
    <button onClick={() => setDarkMode(!darkMode)}>
      {darkMode ? '🌙 Dark' : '☀️ Light'} Mode
    </button>
  );
}

useFetch Hook

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    
    setLoading(true);
    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error(res.statusText);
        return res.json();
      })
      .then(data => {
        setData(data);
        setError(null);
      })
      .catch(err => {
        if (err.name !== 'AbortError') setError(err.message);
      })
      .finally(() => setLoading(false));

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

// Usage:
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
  
  if (loading) return <Spinner />;
  if (error) return <Error message={error} />;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
    </div>
  );
}

Managing Global State with Context API

1. Creating Context

import { createContext, useContext, useState } from 'react';

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (userData) => setUser(userData);
  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  return useContext(AuthContext);
}

2. Using Context

// Wrap your app
function App() {
  return (
    <AuthProvider>
      <Router>
        <Header />
        <MainContent />
      </Router>
    </AuthProvider>
  );
}

// In any component
function Header() {
  const { user, logout } = useAuth();
  
  return (
    <header>
      {user ? (
        <button onClick={logout}>Logout {user.name}</button>
      ) : (
        <LoginButton />
      )}
    </header>
  );
}

Adding Routing with React Router

Basic Setup (v6)

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>
      
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route path="/users/:id" element={<UserProfile />} />
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </BrowserRouter>
  );
}

Protected Routes

function ProtectedRoute({ children }) {
  const { user } = useAuth();
  
  if (!user) {
    return <Navigate to="/login" replace />;
  }
  
  return children;
}

// Usage:
<Route
  path="/dashboard"
  element={
    <ProtectedRoute>
      <Dashboard />
    </ProtectedRoute>
  }
/>

Practical Example: E-Commerce App

// hooks/useCart.js
export function useCart() {
  const [cart, setCart] = useState([]);

  const addToCart = (product) => {
    setCart(prev => [...prev, product]);
  };

  const removeFromCart = (productId) => {
    setCart(prev => prev.filter(item => item.id !== productId));
  };

  return { cart, addToCart, removeFromCart };
}

// context/CartContext.js
const CartContext = createContext();
export const CartProvider = ({ children }) => {
  const cart = useCart();
  return <CartContext.Provider value={cart}>{children}</CartContext.Provider>;
};
export const useCartContext = () => useContext(CartContext);

// App.js
function App() {
  return (
    <CartProvider>
      <BrowserRouter>
        <Header />
        <Routes>
          <Route path="/" element={<ProductListing />} />
          <Route path="/cart" element={<CartPage />} />
        </Routes>
      </BrowserRouter>
    </CartProvider>
  );
}

What’s Next?

In Part 6, we’ll explore:
Advanced React patterns (Render Props, HOCs)
Performance optimization techniques
Testing React apps with Jest and React Testing Library

Exercise for You

Create a useDebounce hook that delays updating a value until after a specified timeout. Use it to implement a search input that only triggers API calls after the user stops typing for 500ms.


Enjoyed this tutorial? Subscribe for Part 6! 🔥

Questions? Ask in the comments below!


Series Navigation:

🔹 Part 1: Introduction to React
🔹 Part 2: JSX, Components & Props
🔹 Part 3: State and Events
🔹 Part 4: useEffect & Conditional Rendering
🔹 Part 5: Custom Hooks & Context API (You’re here)
🔹 Part 6: Advanced Patterns & Optimization

Leave a Comment

Your email address will not be published. Required fields are marked *