Mastering Custom Hooks: Enhancing Your React Projects

Kapil Jhajhria
TechVerito
Published in
3 min readFeb 23, 2024

--

What’s a Custom Hook?

A custom hook is a JavaScript function that starts with the word “use” and allow you to extract component logic into reusable functions. This makes your code cleaner and more maintainable.

In this article, I will be covering various scenarios where you could benefit from using a custom hook. This guide will walk you through the process of creating custom hooks and then demonstrate how to effectively utilize them in your React projects. Throughout this tutorial, we’ll primarily work with TypeScript and harness the power of generics to ensure type safety and flexibility in your code.

useFetch

Have you ever needed to fetch data from an API in your project and found yourself writing the same fetching code over and over again? You could either use an awesome package called react-query or create your own hook as per your requirement.

import { useEffect, useState } from 'react';

interface ResponseData<T> {
data: T | null;
error: Error | null;
loading: boolean;
}

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

useEffect(() => {
fetch(url).then((response) => {
if (!response.ok) {
throw new Error('Network request failed');
}
return response.json();
}).then((result) => {
setData(result);
setError(null);
}).catch((err) => {
setError(err);
setData(null);
}).finally(() => {
setLoading(false);
});
}, [url]);

return { data, error, loading };
}

export default useFetch;

Lets use our custom hook now

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

interface Product {
id: number;
title: string;
price: number;
}

function ProductList() {
const { data, error, loading } = useFetch<Product[]>('https://fakestoreapi.com/products');

if (loading) {
return <p>Loading...</p>;
}

if (error) {
return <p>Error: {error.message}</p>;
}

if (!data) {
return null;
}

return (
<div>
<h2>Product List</h2>
<ul>
{data.map((product) => (
<li key={product.id}>
<strong>{product.title}</strong> - ${product.price}
</li>
))}
</ul>
</div>
);
}

export default ProductList;

useAuth

F or apps needing user authentication, a custom useAuth hook can simplify the process by centralising authentication logic for easy reuse and maintenance.

import { useState, useEffect } from 'react';

interface User {
id: number;
username: string;
}

interface AuthState {
user: User | null;
isAuthenticated: boolean;
}

function useAuth() {
const [authState, setAuthState] = useState<AuthState>({
user: null,
isAuthenticated: false,
});

useEffect(() => {
const token = localStorage.getItem('authToken');

if (token) {
const user = JSON.parse(atob(token.split('.')[1]));
setAuthState({ user, isAuthenticated: true });
} else {
setAuthState({ user: null, isAuthenticated: false });
}
}, []);

const login = (user: User, token: string) => {
localStorage.setItem('authToken', token);
setAuthState({ user, isAuthenticated: true });
};

const logout = () => {
localStorage.removeItem('authToken');
setAuthState({ user: null, isAuthenticated: false });
};

return { ...authState, login, logout };
}

export default useAuth;

Lets look at the example now

import React from 'react';
import useAuth from './useAuth';

function AuthExample() {
const { user, isAuthenticated, login, logout } = useAuth();

const handleLogin = () => {
const fakeUser = { id: 1, username: 'john_doe' };
const fakeToken = btoa(JSON.stringify(fakeUser));
login(fakeUser, fakeToken);
};

const handleLogout = () => {
logout();
};

return (
<div>
<h2>Authentication Example</h2>
{isAuthenticated ? (
<div>
<p>Welcome, {user?.username}!</p>
<button onClick={handleLogout}>Logout</button>
</div>
) : (
<div>
<p>You are not logged in.</p>
<button onClick={handleLogin}>Login</button>
</div>
)}
</div>
);
}

export default AuthExample;

useLocalStorage

Data persistence is a crucial aspect of many web applications. So lets create a hook to help us with that.

import { useState, useEffect } from 'react';

function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Error accessing local storage:', error);
return initialValue;
}
});

useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error('Error accessing local storage:', error);
}
}, [key, storedValue]);

return [storedValue, setStoredValue] as const;
}

export default useLocalStorage;
import React from 'react';
import useLocalStorage from './useLocalStorage';

function LocalStorageExample() {
const [storedValue, setStoredValue] = useLocalStorage<string>('myData', 'default');

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setStoredValue(event.target.value);
};

return (
<div>
<h2>Local Storage Example</h2>
<p>Stored Value: {storedValue}</p>
<input type="text" value={storedValue} onChange={handleInputChange} />
</div>
);
}

export default LocalStorageExample;

In conclusion, custom hooks in React are a powerful tool for organising and reusing your component logic. By creating custom hooks, you can keep your code cleaner and more maintainable.

--

--