Nội dung

DaiPhan

DaiPhan

Full-Stack Developer

Full-stack developer passionate about modern web technologies, best practices, and sharing knowledge with the community.

Skills & Expertise

JavaScript TypeScript React Node.js DevOps
150+
Articles
50k+
Readers
4.9
Rating

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

  1. Naming Convention: Bắt đầu với use và mô tả rõ functionality
  2. Single Responsibility: Mỗi hook nên làm một việc tốt
  3. Dependency Array: Luôn cẩn thận với dependencies trong useEffect
  4. Error Handling: Xử lý errors gracefully trong async hooks
  5. Documentation: Comment và document rõ ràng

❌ Common Pitfalls

  1. Stale Closures: Cẩn thận với closures trong useCallback/useMemo
  2. Infinite Loops: Tránh tạo infinite loops trong useEffect
  3. Missing Dependencies: Luôn include đầy đủ dependencies
  4. 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.