Advanced React Hooks: Custom Hooks for Complex Logic

React hooks have transformed how we manage state and side effects in functional components, enabling more reusable and maintainable code. While React provides built-in hooks like useState, useEffect, and useContext, custom hooks allow you to encapsulate complex logic and share it across components. This article will explore how to create and use custom hooks for managing complex logic in React applications.

M Zeeshan

8/16/20243 min read

silver imac on white table
silver imac on white table

What Are Custom Hooks?

Custom hooks are JavaScript functions that leverage React's built-in hooks to encapsulate reusable logic. They allow you to extract component logic into reusable functions, making your components cleaner and more focused on rendering.

Benefits of Custom Hooks:

  • Reusability: Share logic between multiple components without duplication.

  • Encapsulation: Isolate complex logic, making components easier to understand.

  • Composition: Combine multiple hooks to create sophisticated behaviors.

Creating a Custom Hook

Let’s start with a basic example of a custom hook. Suppose you need to manage form state in several components. Instead of duplicating the state management logic, you can create a custom hook to handle it.

Example: useForm Hook

javascript

Copy code

import { useState } from 'react'; function useForm(initialValues) { const [values, setValues] = useState(initialValues); const handleChange = (e) => { const { name, value } = e.target; setValues((prevValues) => ({ ...prevValues, [name]: value })); }; const reset = () => setValues(initialValues); return { values, handleChange, reset }; } export default useForm;

In this useForm hook:

  • values holds the form state.

  • handleChange updates the state when form inputs change.

  • reset resets the form to its initial values.

You can use this hook in your form components:

javascript

Copy code

import React from 'react'; import useForm from './useForm'; function MyForm() { const { values, handleChange, reset } = useForm({ name: '', email: '' }); const handleSubmit = (e) => { e.preventDefault(); console.log(values); reset(); }; return ( <form onSubmit={handleSubmit}> <input type="text" name="name" value={values.name} onChange={handleChange} placeholder="Name" /> <input type="email" name="email" value={values.email} onChange={handleChange} placeholder="Email" /> <button type="submit">Submit</button> </form> ); } export default MyForm;

Advanced Custom Hook Patterns

Custom hooks can get more complex as your needs evolve. Here are some advanced patterns:

1. Using Custom Hooks for Data Fetching

You can create a custom hook to handle data fetching logic, including loading and error states.

Example: useFetch Hook

javascript

Copy code

import { useState, useEffect } from 'react'; function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { 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 }; } export default useFetch;

You can use this hook in components like so:

javascript

Copy code

import React from 'react'; import useFetch from './useFetch'; function DataDisplay({ url }) { const { data, loading, error } = useFetch(url); if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return <div>Data: {JSON.stringify(data)}</div>; } export default DataDisplay;

2. Combining Multiple Hooks

Sometimes you need to combine multiple hooks to handle complex scenarios.

Example: useAuth Hook

javascript

Copy code

import { useState, useEffect } from 'react'; function useAuth() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const checkAuth = async () => { try { const response = await fetch('/api/check-auth'); if (!response.ok) { throw new Error('Authentication check failed'); } const result = await response.json(); setUser(result.user); } catch (err) { setError(err); } finally { setLoading(false); } }; checkAuth(); }, []); const login = async (credentials) => { try { const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify(credentials), headers: { 'Content-Type': 'application/json' }, }); if (!response.ok) { throw new Error('Login failed'); } const result = await response.json(); setUser(result.user); } catch (err) { setError(err); } }; const logout = async () => { try { await fetch('/api/logout', { method: 'POST' }); setUser(null); } catch (err) { setError(err); } }; return { user, loading, error, login, logout }; } export default useAuth;

This useAuth hook combines authentication checks, login, and logout functionality, providing a comprehensive solution for user authentication.

3. Managing Complex State with Reducers

For more complex state management within a custom hook, you can use the useReducer hook.

Example: useTodo Hook

javascript

Copy code

import { useReducer, useCallback } from 'react'; const initialState = { todos: [], loading: false, error: null }; function reducer(state, action) { switch (action.type) { case 'FETCH_START': return { ...state, loading: true }; case 'FETCH_SUCCESS': return { ...state, todos: action.payload, loading: false }; case 'FETCH_ERROR': return { ...state, error: action.payload, loading: false }; default: return state; } } function useTodo() { const [state, dispatch] = useReducer(reducer, initialState); const fetchTodos = useCallback(async () => { dispatch({ type: 'FETCH_START' }); try { const response = await fetch('/api/todos'); if (!response.ok) throw new Error('Network error'); const todos = await response.json(); dispatch({ type: 'FETCH_SUCCESS', payload: todos }); } catch (error) { dispatch({ type: 'FETCH_ERROR', payload: error }); } }, []); return { todos: state.todos, loading: state.loading, error: state.error, fetchTodos }; } export default useTodo;

The useTodo hook uses useReducer to manage the state of fetching todos, providing a structured approach to complex state logic.