React Hooks Patterns: Custom Hooks và Advanced Patterns
Tìm hiểu các patterns với React Hooks, custom hooks và advanced patterns cho ứng dụng React hiệu quả
React Hooks Patterns: Custom Hooks và Advanced Patterns
React Hooks đã revolutionized cách chúng ta viết React components. Trong bài viết này, chúng ta sẽ explore các patterns advanced với hooks.
1. Custom Hooks Fundamentals
Tại sao cần Custom Hooks?
- Reusability: Share logic giữa components
- Separation of Concerns: Tách biệt business logic khỏi UI
- Testability: Dễ test logic độc lập
- Readability: Code sạch sẽ và dễ maintain
Ví dụ Custom Hook cơ bản
// hooks/useCounter.js
import { useState, useCallback } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []);
const decrement = useCallback(() => {
setCount(prev => prev - 1);
}, []);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return {
count,
increment,
decrement,
reset
};
}
export default useCounter;
2. Advanced Hook Patterns
1. useReducer + useContext Pattern
// context/AppContext.js
import { createContext, useContext, useReducer } from 'react';
const AppContext = createContext();
const initialState = {
user: null,
theme: 'light',
notifications: []
};
function appReducer(state, action) {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'TOGGLE_THEME':
return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
case 'ADD_NOTIFICATION':
return {
...state,
notifications: [...state.notifications, action.payload]
};
default:
return state;
}
}
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}
export function useApp() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within AppProvider');
}
return context;
}
2. useCallback + useMemo Optimization Pattern
// hooks/useOptimizedList.js
import { useState, useCallback, useMemo } from 'react';
function useOptimizedList(items) {
const [filter, setFilter] = useState('');
const [sortOrder, setSortOrder] = useState('asc');
// Memoized filtered items
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// Memoized sorted items
const sortedItems = useMemo(() => {
return [...filteredItems].sort((a, b) => {
if (sortOrder === 'asc') {
return a.name.localeCompare(b.name);
}
return b.name.localeCompare(a.name);
});
}, [filteredItems, sortOrder]);
// Memoized callbacks
const handleFilterChange = useCallback((newFilter) => {
setFilter(newFilter);
}, []);
const handleSortToggle = useCallback(() => {
setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
}, []);
return {
items: sortedItems,
filter,
sortOrder,
onFilterChange: handleFilterChange,
onSortToggle: handleSortToggle
};
}
3. useRef Pattern cho DOM manipulation
// hooks/useClickOutside.js
import { useRef, useEffect } from 'react';
function useClickOutside(callback) {
const ref = useRef(null);
useEffect(() => {
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
callback();
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [callback]);
return ref;
}
// Usage
function Dropdown({ isOpen, onClose, children }) {
const dropdownRef = useClickOutside(onClose);
if (!isOpen) return null;
return (
<div ref={dropdownRef} className="dropdown">
{children}
</div>
);
}
3. Async Data Fetching Patterns
useAsync Hook Generic
// hooks/useAsync.js
import { useState, useEffect, useCallback } from 'react';
function useAsync(asyncFunction, immediate = true) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const execute = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await asyncFunction();
setData(response);
return response;
} catch (err) {
setError(err);
throw err;
} finally {
setLoading(false);
}
}, [asyncFunction]);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { data, loading, error, execute };
}
// Usage
function UserProfile({ userId }) {
const fetchUser = useCallback(async () => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}, [userId]);
const { data: user, loading, error } = useAsync(fetchUser);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return null;
return <div>{user.name}</div>;
}
4. State Management Patterns
useLocalStorage Hook
// hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Get initial value from localStorage or use provided initialValue
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error loading localStorage key "${key}":`, error);
return initialValue;
}
});
// Update localStorage when state changes
const setValue = (value) => {
try {
// Allow value to be a function (similar to useState)
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue];
}
useDebounce Hook
// hooks/useDebounce.js
import { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cleanup timeout on value change
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Usage
function SearchBox({ onSearch }) {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
onSearch(debouncedSearchTerm);
}
}, [debouncedSearchTerm, onSearch]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
5. Testing Custom Hooks
Ví dụ test cho useCounter
// hooks/__tests__/useCounter.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from '@/useCounter';
describe('useCounter', () => {
test('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
test('should initialize with custom value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should decrement counter', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
test('should reset to initial value', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(5);
});
});
6. Best Practices và Common Pitfalls
✅ Best Practices
- Naming Convention: Bắt đầu với
usevà mô tả rõ functionality - Single Responsibility: Mỗi hook nên làm một việc tốt
- Dependency Array: Luôn cẩn thận với dependencies trong useEffect
- Error Handling: Xử lý errors gracefully trong async hooks
- Documentation: Comment và document rõ ràng
❌ Common Pitfalls
- Stale Closures: Cẩn thận với closures trong useCallback/useMemo
- Infinite Loops: Tránh tạo infinite loops trong useEffect
- Missing Dependencies: Luôn include đầy đủ dependencies
- Over-optimization: Đừng premature optimize với useMemo/useCallback
7. Kết luận
React Hooks patterns giúp chúng ta:
- Reusable Logic: Share logic giữa components
- Clean Code: Tách biệt concerns và improve readability
- Better Performance: Optimize với useMemo và useCallback
- Easier Testing: Test logic độc lập
💡 Pro Tip: Luôn bắt đầu với simple patterns và evolve khi cần thiết. Đừng over-engineer!
Trong bài tiếp theo, chúng ta sẽ explore State Management với Zustand và so sánh với Redux và Context API.