React Hooks have revolutionized how we build React components, making it possible to use state and other React features without writing a class. In this article, we'll move beyond the basics and explore advanced hook patterns and custom hooks that can solve real-world problems.
Advanced useEffect Patterns
The useEffect hook is one of the most powerful but also most misunderstood hooks. Here are some advanced patterns:
Cleaning Up Resources
useEffect(() => {
const subscription = subscribeToData(dataId);
// Return a cleanup function
return () => {
subscription.unsubscribe();
};
}, [dataId]);
Debouncing in useEffect
useEffect(() => {
const handler = setTimeout(() => {
// Perform an action after the user stops typing
fetchResults(searchTerm);
}, 500);
return () => {
clearTimeout(handler);
};
}, [searchTerm]);
Custom Hooks for Common Patterns
Custom hooks allow you to extract component logic into reusable functions. Here are some useful 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) {
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];
}
useFetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const json = await response.json();
setData(json);
setLoading(false);
} catch (error) {
setError(error.message);
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
The useReducer Hook for Complex State Logic
When component state becomes complex, useReducer can provide a more structured way to manage it:
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.payload, completed: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
}
function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, []);
const addTodo = text => {
dispatch({ type: 'ADD_TODO', payload: text });
};
const toggleTodo = id => {
dispatch({ type: 'TOGGLE_TODO', payload: id });
};
const deleteTodo = id => {
dispatch({ type: 'DELETE_TODO', payload: id });
};
// Rest of component...
}
Conclusion
React Hooks offer a powerful way to organize and reuse stateful logic in your components. By mastering advanced patterns and creating custom hooks, you can write more maintainable, concise, and powerful React applications. Keep experimenting with different hook patterns to find what works best for your specific use cases.