React Custom Hooks with TypeScript: Real-World Examples You’ll Actually Use
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:
useFetch→ Fetch and manage API data.useLocalStorage→ Persist state across sessions.useAuth→ Manage user authentication.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)?


