Skip to main content

Custom Hooks Best Practices

Answer​

Custom hooks are JavaScript functions that start with "use" and can call other hooks. They allow you to extract and reuse stateful logic between components.

Basic Structure​

// Custom hook naming: use[Name]
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);

const increment = useCallback(() => setCount((c) => c + 1), []);
const decrement = useCallback(() => setCount((c) => c - 1), []);
const reset = useCallback(() => setCount(initialValue), [initialValue]);

return { count, increment, decrement, reset };
}

// Usage
function Counter() {
const { count, increment, decrement } = useCounter(0);

return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}

Common Custom Hooks​

useLocalStorage​

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

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

return [storedValue, setValue];
}

// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage("theme", "light");
return <button onClick={() => setTheme("dark")}>Dark Mode</button>;
}

useFetch​

function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
let isMounted = true;
setLoading(true);

fetch(url)
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then((data) => {
if (isMounted) {
setData(data);
setLoading(false);
}
})
.catch((error) => {
if (isMounted) {
setError(error);
setLoading(false);
}
});

return () => {
isMounted = false;
};
}, [url]);

return { data, loading, error };
}

// Usage
function Users() {
const { data: users, loading, error } = useFetch("/api/users");

if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}

useDebounce​

function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);

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

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

return debouncedValue;
}

// Usage
function Search() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 500);

useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);

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

useMediaQuery​

function useMediaQuery(query) {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
);

useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handler = (e) => setMatches(e.matches);

mediaQuery.addEventListener("change", handler);
return () => mediaQuery.removeEventListener("change", handler);
}, [query]);

return matches;
}

// Usage
function ResponsiveComponent() {
const isMobile = useMediaQuery("(max-width: 768px)");
return isMobile ? <MobileLayout /> : <DesktopLayout />;
}

useClickOutside​

function useClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
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 dropdownRef = useRef(null);

useClickOutside(dropdownRef, () => setIsOpen(false));

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

Best Practices​

// 1. Name starts with "use"
function useMyHook() {} // ✅
function myHook() {} // ❌ Won't work with React's rules

// 2. Return consistent types
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue((v) => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);

return { value, toggle, setTrue, setFalse };
}

// 3. Handle cleanup for side effects
// 4. Memoize functions to maintain referential equality
// 5. Document the hook's API with TypeScript or JSDoc

Key Points​

  • Custom hooks extract reusable stateful logic
  • Must start with "use" prefix
  • Can call other hooks (useState, useEffect, etc.)
  • Return values, not JSX
  • Great for: data fetching, form handling, subscriptions
  • Keep hooks focused on one concern
  • Memoize returned functions with useCallback