Skip to main content

Custom Hooks

Custom hooks are a powerful feature in React that allows you to extract and reuse stateful logic between components. In ABP React, custom hooks are extensively used to encapsulate API calls, authentication logic, and other common functionality.

๐ŸŽฏ What are Custom Hooks?โ€‹

Custom hooks are JavaScript functions that:

  • Start with the word "use" (React convention)
  • Can use other React hooks internally
  • Return values, functions, or both
  • Can accept parameters
  • Follow the same rules as React hooks

๐Ÿ—๏ธ Hook Structureโ€‹

Basic Hook Templateโ€‹

import { useState, useEffect } from 'react';

export const useCustomHook = (initialValue: any) => {
// State declarations
const [state, setState] = useState(initialValue);

// Effects
useEffect(() => {
// Side effects
}, []);

// Helper functions
const updateState = (newValue: any) => {
setState(newValue);
};

// Return values and functions
return {
state,
updateState,
};
};

๐Ÿ”ง API Hooksโ€‹

useApi Hookโ€‹

A generic hook for making API calls:

import { useState, useEffect } from 'react';
import { api } from '@/lib/api';

interface UseApiOptions<T> {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: any;
dependencies?: any[];
enabled?: boolean;
}

export const useApi = <T>({
url,
method = 'GET',
body,
dependencies = [],
enabled = true,
}: UseApiOptions<T>) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);

const fetchData = async () => {
if (!enabled) return;

setLoading(true);
setError(null);

try {
const response = await api.request({
url,
method,
data: body,
});
setData(response.data);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};

useEffect(() => {
fetchData();
}, dependencies);

const refetch = () => {
fetchData();
};

return {
data,
loading,
error,
refetch,
};
};

useMutation Hookโ€‹

For handling API mutations (POST, PUT, DELETE):

import { useState } from 'react';
import { api } from '@/lib/api';

interface UseMutationOptions<T, R> {
url: string;
method?: 'POST' | 'PUT' | 'DELETE';
onSuccess?: (data: R) => void;
onError?: (error: Error) => void;
}

export const useMutation = <T, R>({
url,
method = 'POST',
onSuccess,
onError,
}: UseMutationOptions<T, R>) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [data, setData] = useState<R | null>(null);

const mutate = async (body: T) => {
setLoading(true);
setError(null);

try {
const response = await api.request({
url,
method,
data: body,
});

setData(response.data);
onSuccess?.(response.data);
return response.data;
} catch (err) {
const error = err as Error;
setError(error);
onError?.(error);
throw error;
} finally {
setLoading(false);
}
};

return {
mutate,
loading,
error,
data,
};
};

๐Ÿ” Authentication Hooksโ€‹

useAuth Hookโ€‹

Manage authentication state:

import { useState, useEffect, useContext } from 'react';
import { AuthContext } from '@/contexts/AuthContext';

export const useAuth = () => {
const { user, login, logout, refreshToken } = useContext(AuthContext);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
// Check if user is authenticated on mount
const checkAuth = async () => {
try {
await refreshToken();
} catch (error) {
// User is not authenticated
} finally {
setIsLoading(false);
}
};

checkAuth();
}, []);

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

usePermission Hookโ€‹

Check user permissions:

import { useContext, useMemo } from 'react';
import { AuthContext } from '@/contexts/AuthContext';

export const usePermission = (permission: string) => {
const { user } = useContext(AuthContext);

return useMemo(() => {
if (!user || !user.permissions) {
return false;
}

return user.permissions.includes(permission);
}, [user, permission]);
};

usePermissions Hookโ€‹

Check multiple permissions at once:

import { useContext, useMemo } from 'react';
import { AuthContext } from '@/contexts/AuthContext';

export const usePermissions = (permissions: string[]) => {
const { user } = useContext(AuthContext);

return useMemo(() => {
if (!user || !user.permissions) {
return permissions.reduce((acc, permission) => {
acc[permission] = false;
return acc;
}, {} as Record<string, boolean>);
}

return permissions.reduce((acc, permission) => {
acc[permission] = user.permissions.includes(permission);
return acc;
}, {} as Record<string, boolean>);
}, [user, permissions]);
};

๐Ÿ“Š Data Management Hooksโ€‹

useLocalStorage Hookโ€‹

Persist data in localStorage:

import { useState, useEffect } from 'react';

export const useLocalStorage = <T>(
key: string,
initialValue: T
): [T, (value: T) => void] => {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});

const setValue = (value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};

return [storedValue, setValue];
};

useDebounce Hookโ€‹

Debounce values for search inputs:

import { useState, useEffect } from 'react';

export const useDebounce = <T>(value: T, delay: number): T => {
const [debouncedValue, setDebouncedValue] = useState<T>(value);

useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
clearTimeout(handler);
};
}, [value, delay]);

return debouncedValue;
};

usePrevious Hookโ€‹

Track previous values:

import { useRef, useEffect } from 'react';

export const usePrevious = <T>(value: T): T | undefined => {
const ref = useRef<T>();

useEffect(() => {
ref.current = value;
});

return ref.current;
};

๐ŸŽจ UI Hooksโ€‹

useMediaQuery Hookโ€‹

Respond to media queries:

import { useState, useEffect } from 'react';

export const useMediaQuery = (query: string): boolean => {
const [matches, setMatches] = useState(false);

useEffect(() => {
const media = window.matchMedia(query);

if (media.matches !== matches) {
setMatches(media.matches);
}

const listener = () => setMatches(media.matches);
media.addEventListener('change', listener);

return () => media.removeEventListener('change', listener);
}, [matches, query]);

return matches;
};

useClickOutside Hookโ€‹

Detect clicks outside an element:

import { useEffect, RefObject } from 'react';

export const useClickOutside = (
ref: RefObject<HTMLElement>,
handler: (event: MouseEvent | TouchEvent) => void
) => {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler(event);
};

document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);

return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
};

useScrollPosition Hookโ€‹

Track scroll position:

import { useState, useEffect } from 'react';

export const useScrollPosition = () => {
const [scrollPosition, setScrollPosition] = useState(0);

useEffect(() => {
const updatePosition = () => {
setScrollPosition(window.pageYOffset);
};

window.addEventListener('scroll', updatePosition);
updatePosition();

return () => window.removeEventListener('scroll', updatePosition);
}, []);

return scrollPosition;
};

๐Ÿ”„ State Management Hooksโ€‹

useReducer Hookโ€‹

Complex state management:

import { useReducer, useCallback } from 'react';

interface FormState {
values: Record<string, any>;
errors: Record<string, string>;
touched: Record<string, boolean>;
isSubmitting: boolean;
}

type FormAction =
| { type: 'SET_VALUE'; field: string; value: any }
| { type: 'SET_ERROR'; field: string; error: string }
| { type: 'SET_TOUCHED'; field: string }
| { type: 'SET_SUBMITTING'; isSubmitting: boolean }
| { type: 'RESET' };

const formReducer = (state: FormState, action: FormAction): FormState => {
switch (action.type) {
case 'SET_VALUE':
return {
...state,
values: { ...state.values, [action.field]: action.value },
};
case 'SET_ERROR':
return {
...state,
errors: { ...state.errors, [action.field]: action.error },
};
case 'SET_TOUCHED':
return {
...state,
touched: { ...state.touched, [action.field]: true },
};
case 'SET_SUBMITTING':
return { ...state, isSubmitting: action.isSubmitting };
case 'RESET':
return {
values: {},
errors: {},
touched: {},
isSubmitting: false,
};
default:
return state;
}
};

export const useForm = (initialValues: Record<string, any> = {}) => {
const [state, dispatch] = useReducer(formReducer, {
values: initialValues,
errors: {},
touched: {},
isSubmitting: false,
});

const setValue = useCallback((field: string, value: any) => {
dispatch({ type: 'SET_VALUE', field, value });
}, []);

const setError = useCallback((field: string, error: string) => {
dispatch({ type: 'SET_ERROR', field, error });
}, []);

const setTouched = useCallback((field: string) => {
dispatch({ type: 'SET_TOUCHED', field });
}, []);

const setSubmitting = useCallback((isSubmitting: boolean) => {
dispatch({ type: 'SET_SUBMITTING', isSubmitting });
}, []);

const reset = useCallback(() => {
dispatch({ type: 'RESET' });
}, []);

return {
...state,
setValue,
setError,
setTouched,
setSubmitting,
reset,
};
};

๐Ÿงช Testing Hooksโ€‹

Testing Custom Hooksโ€‹

import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
it('should increment counter', () => {
const { result } = renderHook(() => useCounter(0));

act(() => {
result.current.increment();
});

expect(result.current.count).toBe(1);
});

it('should decrement counter', () => {
const { result } = renderHook(() => useCounter(1));

act(() => {
result.current.decrement();
});

expect(result.current.count).toBe(0);
});
});

๐Ÿ“ Best Practicesโ€‹

1. Naming Conventionsโ€‹

  • Always start with "use"
  • Use descriptive names
  • Follow camelCase
// Good
export const useUserProfile = () => { /* ... */ };
export const useApiCall = () => { /* ... */ };

// Avoid
export const getUserProfile = () => { /* ... */ };
export const apiCall = () => { /* ... */ };

2. Return Valuesโ€‹

  • Return objects for multiple values
  • Use consistent return types
  • Document return values
// Good
export const useUser = (id: string) => {
// ... implementation
return {
user,
loading,
error,
refetch,
};
};

// Avoid
export const useUser = (id: string) => {
// ... implementation
return [user, loading, error, refetch]; // Array is less clear
};

3. Error Handlingโ€‹

export const useApiWithError = (url: string) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);

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

try {
const response = await api.get(url);
setData(response.data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};

return { data, error, loading, refetch: fetchData };
};

4. Dependenciesโ€‹

  • Include all dependencies in useEffect
  • Use useCallback for functions passed as props
  • Use useMemo for expensive calculations
export const useExpensiveCalculation = (data: any[]) => {
const result = useMemo(() => {
return data.reduce((acc, item) => acc + item.value, 0);
}, [data]);

const handleClick = useCallback(() => {
console.log('Result:', result);
}, [result]);

return { result, handleClick };
};

Custom hooks are a fundamental part of ABP React's architecture, providing reusable logic across components. By following these patterns and best practices, you can create maintainable and efficient custom hooks that enhance your application's functionality.