React Custom Hooks Best Practices: With Example Usecases

Muhammad Syakirurohman
Frontend Engineer
Published on
React Custom Hooks Best Practices: With Example Usecases

If you’re a React developer, you’ve likely encountered a situation where you need to share logic across components. In this post, we’ll explore what custom hooks are, when to use them, best practices for writing them, and some common use cases that will make your React applications cleaner and more maintainable.

Related posts about React

  1. How to Start & Setup a React project in 2024
  2. React Functional Component with Hooks
  3. React Conditional Rendering (If Else) Best Practices

What is a Custom Hook in React?

A Custom hook is a reusable function with stateful logic that can be shared across React components. They start with the prefix use and can call other hooks inside them, making it possible to abstract away complex state and effect logic into simple, reusable functions.

React custom hooks are perfect for situations where you need to share logic between different parts of your application without duplicating code. This not only keeps your components cleaner but also promotes a more modular codebase.

Note that a custom hook is different with a regular javascript reusable function. Custom hook is stateful, meaning that you should use react state with useState hook or other built-in hooks.

When to Use Custom Hooks in React

Custom hooks are a great fit whenever you need to reuse stateful logic across components, especially for complex tasks like data fetching, form handling, and authentication. They simplify your component code, enhance readability, and make testing and maintenance easier.

Another prime use case for custom hooks is when you find yourself writing the same code in multiple places. Instead of copying and pasting the same logic, you can extract it into a custom hook and reuse it wherever needed. This promotes the DRY (Don’t Repeat Yourself) principle, making your codebase more efficient and less error-prone.

Best Practices for Writing Custom Hooks

When creating custom hooks, there are several best practices to keep in mind to ensure they are effective and maintainable. Here are some key guidelines:

1. Start with use

Always start the name of your custom hook with use. This is a convention that helps other React developers identify these functions as hooks, ensuring the rules of hooks are applied correctly.

2. Keep Hooks Pure

Ensure that your hooks are pure functions. Avoid side effects directly inside your hooks; instead, manage them using useEffect or similar hooks.

Side effects are any operations or behaviors that occur in a component after rendering, and that don’t directly impact the current component render cycle.

3. Avoid Unnecessary Renders

Use memoization techniques like useMemo or useCallback to prevent your hooks from causing unnecessary re-renders, especially when dealing with expensive calculations or complex state updates.

Leverage built-in hooks such as useState, useEffect, useReducer, and useCallback to manage state and side effects within your custom hooks.

4. Return Consistent Value Types

Return an array or object with consistent value types from your hooks containing the states, functions, or any value you want to expose. This makes it clear what the hook provides and how to use it.

5. Write Tests and Document your Custom Hooks

Ensure your custom hooks are well-tested. Use tools like React Testing Library and Jest to write tests that validate the behavior of your hooks.

Provide clear documentation for your custom hooks. Explain what they do, their parameters, and their return values to make them easier for others (and yourself) to use and maintain.

6. Keep It Simple

Avoid making your hooks too complex. If a hook starts becoming too complicated, consider breaking it down into smaller, more focused hooks.

Make sure your hooks only have single responsibility.

7. Handle Errors Gracefully

Handle errors gracefully within your hooks. This ensures that your components using these hooks can handle unexpected scenarios without breaking.

Example Use Cases of Custom Hooks

Here are some common use cases for custom hooks that you might encounter in your React projects:

1. Data Fetching

A custom hook for fetching data from an API endpoint can be reused across different components that need to fetch and display data.

import { useState, useEffect } from 'react';
 
function useFetch(url) {
	const [data, setData] = useState(null);
	const [loading, setLoading] = useState(true);
	const [error, setError] = useState(null);
 
	useEffect(() => {
		async function fetchData() {
			try {
				const response = await fetch(url);
				if (!response.ok) {
					throw new Error('Network response was not ok');
				}
				const result = await response.json();
				setData(result);
			} catch (err) {
				setError(err);
			} finally {
				setLoading(false);
			}
		}
 
		fetchData();
	}, [url]);
 
	return { data, loading, error };
}
const Component = () => {
	const { data, loading, error } = useFetch('https://example.com/api/path');
 
	if (loading) return <div>Loading...</div>;
	if (error) return <div>Error: {error.message}</div>;
 
	return (
		<div>
			<h1>Data:</h1>
			<pre>{JSON.stringify(data)}</pre>
		</div>
	);
};

2. Form Handling

Custom hooks can manage form state, handle validation, and provide submit handlers, making form management a breeze.

import { useState } from 'react';
 
function useForm(initialValues) {
	const [values, setValues] = useState(initialValues);
	const [errors, setErrors] = useState({});
 
	const handleChange = (event) => {
		const { name, value } = event.target;
		setValues({
			...values,
			[name]: value
		});
	};
 
	const validate = (name, value) => {
		if (value.trim() === '') {
			setErrors((prevErrors) => ({ ...prevErrors, [name]: 'This field is required' }));
		} else {
			setErrors((prevErrors) => ({ ...prevErrors, [name]: '' }));
		}
	};
 
	const handleSubmit = (event, callback) => {
		event.preventDefault();
		if (Object.values(errors).every((err) => err === '')) {
			callback(values);
		}
	};
 
	return { values, handleChange, handleSubmit, validate, errors };
}
const Component = () => {
	const { values, errors, handleChange, handleSubmit } = useForm({ username: '', password: '' }, validate);
 
	const submit = () => {
		alert('Form submitted successfully');
	};
 
	return (
		<form onSubmit={(e) => handleSubmit(e, submit)}>
			<div>
				<label>Username</label>
				<input type='text' name='username' value={values.username} onChange={handleChange} />
				{errors.username && <p>{errors.username}</p>}
			</div>
			<button type='submit'>Submit</button>
		</form>
	);
};

3. Authentication

Managing user authentication state, including login, logout, and checking if a user is authenticated.

import { useState, useEffect } from 'react';
 
function useAuth() {
	const [user, setUser] = useState(null);
 
	useEffect(() => {
		const loggedUser = localStorage.getItem('user');
		if (loggedUser) {
			setUser(JSON.parse(loggedUser));
		}
	}, []);
 
	const login = (userData) => {
		setUser(userData);
		// call login api here
		localStorage.setItem('user', JSON.stringify(userData));
	};
 
	const logout = () => {
		setUser(null);
		localStorage.removeItem('user');
	};
 
	return { user, login, logout };
}

4. Window Size

A custom hook for tracking window size changes, which can be useful for responsive design.

import { useState, useEffect } from 'react';
 
function useWindowSize() {
	const [size, setSize] = useState({
		width: window.innerWidth,
		height: window.innerHeight
	});
 
	useEffect(() => {
		const handleResize = () => {
			setSize({
				width: window.innerWidth,
				height: window.innerHeight
			});
		};
 
		window.addEventListener('resize', handleResize);
		return () => window.removeEventListener('resize', handleResize);
	}, []);
 
	return size;
}

5. Debouncing Input

A custom hook for debouncing input changes, which is useful for search inputs or other scenarios where you want to delay a function call.

import { useState, useEffect } from 'react';
 
function useDebounce(value, delay) {
	const [debouncedValue, setDebouncedValue] = useState(value);
 
	useEffect(() => {
		const handler = setTimeout(() => {
			setDebouncedValue(value);
		}, delay);
 
		return () => {
			clearTimeout(handler);
		};
	}, [value, delay]);
 
	return debouncedValue;
}

Conclusion

Custom hooks can greatly simplify your React code by making it more reusable and maintainable. By adhering to best practices, such as avoiding side effects directly inside hooks and ensuring that you do not mutate arrays or objects directly, you can create hooks that are predictable and easy to test.

If you found this post useful, consider giving it a like, and share it. If you have opinion or more suggestions abou custom hooks, please dont hesitate to post it in the comment.

Thank you, Happy coding!