Skip to main content

Custom Hooks - Reusable Logic

What are Custom Hooks?

Custom hooks let you extract component logic into reusable functions. They start with "use" and can call other hooks.

Basic Custom Hook

function useToggle(initialValue: boolean = false) {
const [value, setValue] = useState(initialValue);

const toggle = useCallback(() => {
setValue(v => !v);
}, []);

return [value, toggle] as const;
}

// Usage
function Modal() {
const [isOpen, toggleOpen] = useToggle(false);

return (
<>
<button onClick={toggleOpen}>Open Modal</button>
{isOpen && <ModalContent onClose={toggleOpen} />}
</>
);
}

useLocalStorage Hook

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

const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};

return [storedValue, setValue] as const;
}

useDebounce Hook

function 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;
}

// Usage
function Search() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);

useEffect(() => {
if (debouncedSearchTerm) {
searchAPI(debouncedSearchTerm).then(setResults);
}
}, [debouncedSearchTerm]);

return (
<input value={searchTerm} onChange={e => setSearchTerm(e.target.value)} />
);
}

useFetch Hook

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

function useFetch\<T\>(url: string) {
const [state, setState] = useState<FetchState\<T\>>({
data: null,
loading: true,
error: null,
});

useEffect(() => {
let cancelled = false;

async function fetchData() {
setState({ data: null, loading: true, error: null });

try {
const response = await fetch(url);
const data = await response.json();

if (!cancelled) {
setState({ data, loading: false, error: null });
}
} catch (error) {
if (!cancelled) {
setState({ data: null, loading: false, error: error as Error });
}
}
}

fetchData();

return () => {
cancelled = true;
};
}, [url]);

return state;
}

// Usage
function UserProfile({ userId }: { userId: string }) {
const { data: user, loading, error } = useFetch<User>(`/api/users/${userId}`);

if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <UserCard user={user!} />;
}

useOnClickOutside Hook

function useOnClickOutside<T extends HTMLElement = HTMLElement>(
ref: RefObject\<T\>,
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]);
}

// Usage
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);

useOnClickOutside(ref, () => setIsOpen(false));

return (
<div ref={ref}>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && <DropdownMenu />}
</div>
);
}

Best Practices

  1. Start with "use" - Convention for hooks
  2. Extract reusable logic - Don't repeat yourself
  3. Compose hooks - Use hooks inside hooks
  4. Return consistent types - Use as const for tuples
  5. Handle cleanup - Return cleanup functions
  6. Type generics - Make hooks flexible

Practice Exercises

  1. Create useAsync for async operations
  2. Build useMediaQuery for responsive design
  3. Implement useForm for form management
  4. Create useWebSocket for real-time data

Next Steps

  • Practice building more custom hooks
  • Explore advanced hook patterns