Sitemap

React Custom Hooks with TypeScript: Real-World Examples You’ll Actually Use

4 min readDec 22, 2025

--

When building React applications, we often find ourselves repeating the same logic across multiple components — fetching data, managing forms, persisting state, handling authentication, and more.

This is where custom hooks shine. They allow us to abstract reusable logic into clean, type-safe functions. With TypeScript, we take it a step further by making them strongly typed and self-documenting.

Get dongolsujata-dev’s stories in your inbox

Join Medium for free to get updates from this writer.

In this post, we’ll build four real-world custom hooks you can start using right away:

  1. useFetch → Fetch and manage API data.
  2. useLocalStorage → Persist state across sessions.
  3. useAuth → Manage user authentication.
  4. useForm → Simplify form state handling.

useFetch — Data Fetching with TypeScript Generics

Fetching data is one of the most common needs. Instead of writing loading/error logic everywhere, we can encapsulate it.

import { useEffect, useState } from "react";

export function useFetch<T>(url: string): {
data: T | null;
loading: boolean;
error: string | null;
} {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);

const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}

const result = (await response.json()) as T;
setData(result);
} catch (err: any) {
setError(err.message || "Something went wrong");
} finally {
setLoading(false);
}
};

fetchData();
}, [url]);

return { data, loading, error };
}

Usage: Suppose we want to fetch a list of users from an API.

import React from "react";
import useFetch from "./useFetch";

interface User {
id: number;
name: string;
email: string;
}

export default function UsersList() {
const { data: users, loading, error } = useFetch<User[]>("https://jsonplaceholder.typicode.com/users");

if (loading) return <p>Loading...</p>;
if (error) return <p style={{ color: "red" }}>Error: {error}</p>;

return (
<ul>
{users?.map((user) => (
<li key={user.id}>
{user.name} — {user.email}
</li>
))}
</ul>
);
}

useLocalStorage — Persist State Between Sessions

Local storage is perfect for settings (like theme or cart items). This hook syncs React state with local storage.

import { useState, useEffect } from "react";

export function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const stored = localStorage.getItem(key);
return stored ? (JSON.parse(stored) as T) : initialValue;
} catch {
return initialValue;
}
});

useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// If localStorage is full or unavailable
console.error("Failed to save to localStorage");
}
}, [key, value]);

return [value, setValue] as const;
}

Usage:

import React from "react";
import { useLocalStorage } from "./useLocalStorage";

export default function ThemeSwitcher() {
const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light");

const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};

return (
<div
style={{
backgroundColor: theme === "light" ? "#f9f9f9" : "#222",
color: theme === "light" ? "#222" : "#f9f9f9",
}}
>
<h1>{theme === "light" ? "🌞 Light Mode" : "🌙 Dark Mode"}</h1>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}

useAuth — Simple Authentication Hook

Authentication is a must-have in most apps. Let’s store user info and manage login/logout with local storage. Here we have reused the useLocalStorage hook from earlier example.

import { useLocalStorage } from "./useLocalStorage";

interface User {
id: string;
name: string;
email: string;
}

export function useAuth() {
const [user, setUser] = useLocalStorage<User | null>("user", null);

const login = (newUser: User) => {
setUser(newUser);
};

const logout = () => {
setUser(null);
};

return { user, login, logout, isAuthenticated: !!user };
}

Usage:

import React, { useState } from "react";
import { useAuth } from "./useAuth";

export default function OnboardingPage() {
const { user, login, logout, isAuthenticated } = useAuth();
const [name, setName] = useState("");
const [email, setEmail] = useState("");

if (isAuthenticated) {
return (
<div>
<h2>Welcome, {user?.name} 👋</h2>
<p>Email: {user?.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}

return (
<form
onSubmit={(e) => {
e.preventDefault();
login({ id: Date.now().toString(), name, email });
}}
>
<input
type="text"
placeholder="Name"
onChange={(e) => setName(e.target.value)}
required
/>
<input
type="email"
placeholder="Email"
onChange={(e) => setEmail(e.target.value)}
required
/>
<button type="submit">Login</button>
</form>
);
}

useForm — Handle Forms with Ease

Managing form inputs can get repetitive. Let’s create a hook to simplify it.

import { useState } from "react";

export function useForm<T extends Record<string, any>>(initialValues: T) {
const [values, setValues] = useState(initialValues);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setValues({ ...values, [e.target.name]: e.target.value });

return { values, handleChange, reset: () => setValues(initialValues) };
}

Usage:

import React from "react";
import { useForm } from "./useForm";

export default function LoginForm() {
const { values, handleChange, reset } = useForm({ email: "", password: "" });

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log(values);
reset();
};

return (
<form onSubmit={handleSubmit}>
<input name="email" value={values.email} onChange={handleChange} />
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
/>
<button type="submit">Login</button>
</form>
);
}

Custom hooks in React are more than just a convenience — they’re a way to write cleaner, reusable, and testable logic.

Next time you repeat logic in React, stop and ask yourself:
👉 “Can this be a custom hook?”

🚀 Hook It Yourself: The fun part of custom hooks is making your own. Why not try useDebounce (for search bars), useInfiniteScroll (for endless feeds), or useOnlineStatus (to detect if you’re offline)?

--

--