Custom hooks are a powerful feature in React that allow you to extract component logic into reusable functions. They're a way to reuse stateful logic between components without adding more components to your tree. In this guide, we'll dive deep into creating custom hooks, their benefits, and best practices.
Custom hooks are JavaScript functions that start with the prefix "use" and may call other hooks. They allow you to extract component logic into reusable functions, making your code more modular and easier to maintain.
Before diving into creating custom hooks, it's crucial to understand the rules of hooks:
Let's create a simple custom hook that manages a counter:
import { useState } from 'react';
function useCounter(initialCount = 0) {
const [count, setCount] = useState(initialCount);
const increment = () => setCount(prevCount => prevCount + 1);
const decrement = () => setCount(prevCount => prevCount - 1);
const reset = () => setCount(initialCount);
return { count, increment, decrement, reset };
}
export default useCounter;
Now you can use this hook in any component:
import React from 'react';
import useCounter from './useCounter';
function CounterComponent() {
const { count, increment, decrement, reset } = useCounter(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
}
You can combine multiple hooks to create more complex custom hooks:
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);
const json = await response.json();
setData(json);
setLoading(false);
} catch (error) {
setError(error);
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
Custom hooks can also accept callbacks as parameters:
import { useState, useCallback } from 'react';
function useToggle(initialState = false) {
const [state, setState] = useState(initialState);
const toggle = useCallback(() => {
setState(prevState => !prevState);
}, []);
return [state, toggle];
}
When your custom hook sets up subscriptions or timers, make sure to clean them up:
import { useState, useEffect } from 'react';
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return width;
}
Naming Convention: Always start your custom hook names with "use" to follow the React convention.
Keep It Simple: Each hook should do one thing well. If a hook becomes too complex, consider breaking it into smaller hooks.
Proper Dependencies: When using useEffect
or useCallback
in your custom hook, make sure to include all necessary dependencies in the dependency array.
TypeScript Support: If you're using TypeScript, add proper typing to your custom hooks for better developer experience:
import { useState } from 'react';
interface UseCounterReturn {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
function useCounter(initialCount: number = 0): UseCounterReturn {
// ... implementation
}
Testing: Write unit tests for your custom hooks using the @testing-library/react-hooks
package:
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
A custom hook to persist state in localStorage:
import { useState } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
}
A hook to check if a media query matches:
import { useState, useEffect } from 'react';
function useMediaQuery(query) {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
if (media.matches !== matches) {
setMatches(media.matches);
}
const listener = () => setMatches(media.matches);
media.addListener(listener);
return () => media.removeListener(listener);
}, [matches, query]);
return matches;
}
A hook to debounce a rapidly changing value:
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;
}
2024 © All rights reserved - buraxta.com