Skip to content

React Custom Hooks

Overview

Custom Hooks are a way React provides for reusing state logic. By creating custom Hooks, we can extract component logic into reusable functions to achieve code modularization and reuse. This chapter will learn how to create and use custom Hooks.

🎣 Basic Custom Hooks

Simple State Management Hook

jsx
// Custom Hook: Counter
function useCounter(initialValue = 0) {
  const [count, setCount] = React.useState(initialValue);

  const increment = React.useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  const decrement = React.useCallback(() => {
    setCount(prev => prev - 1);
  }, []);

  const reset = React.useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);

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

// Using custom Hook
function CounterExample() {
  const counter1 = useCounter(0);
  const counter2 = useCounter(10);

  return (
    <div style={{ padding: '20px' }}>
      <h2>Custom Counter Hook</h2>

      <div style={{ marginBottom: '20px' }}>
        <h3>Counter 1</h3>
        <p>Current value: {counter1.count}</p>
        <button onClick={counter1.increment}>+1</button>
        <button onClick={counter1.decrement}>-1</button>
        <button onClick={counter1.reset}>Reset</button>
      </div>

      <div>
        <h3>Counter 2 (Initial: 10)</h3>
        <p>Current value: {counter2.count}</p>
        <button onClick={counter2.increment}>+1</button>
        <button onClick={counter2.decrement}>-1</button>
        <button onClick={counter2.reset}>Reset</button>
      </div>
    </div>
  );
}

Toggle State Hook

jsx
// Custom Hook: Boolean toggle
function useToggle(initialValue = false) {
  const [value, setValue] = React.useState(initialValue);

  const toggle = React.useCallback(() => {
    setValue(prev => !prev);
  }, []);

  const setTrue = React.useCallback(() => {
    setValue(true);
  }, []);

  const setFalse = React.useCallback(() => {
    setValue(false);
  }, []);

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

// Usage example
function ToggleExample() {
  const modal = useToggle(false);
  const sidebar = useToggle(true);
  const darkMode = useToggle(false);

  return (
    <div style={{
      padding: '20px',
      backgroundColor: darkMode.value ? '#333' : '#fff',
      color: darkMode.value ? '#fff' : '#333',
      minHeight: '400px'
    }}>
      <h2>Toggle State Demo</h2>

      <div style={{ marginBottom: '20px' }}>
        <button onClick={modal.toggle}>
          {modal.value ? 'Close' : 'Open'} Modal
        </button>
        <button onClick={sidebar.toggle} style={{ marginLeft: '10px' }}>
          {sidebar.value ? 'Hide' : 'Show'} Sidebar
        </button>
        <button onClick={darkMode.toggle} style={{ marginLeft: '10px' }}>
          {darkMode.value ? 'Light' : 'Dark'} Mode
        </button>
      </div>

      {sidebar.value && (
        <div style={{
          position: 'fixed',
          left: 0,
          top: 0,
          width: '200px',
          height: '100vh',
          backgroundColor: darkMode.value ? '#444' : '#f8f9fa',
          padding: '20px',
          borderRight: '1px solid #ddd'
        }}>
          <h3>Sidebar</h3>
          <ul>
            <li>Nav Item 1</li>
            <li>Nav Item 2</li>
            <li>Nav Item 3</li>
          </ul>
        </div>
      )}

      {modal.value && (
        <div style={{
          position: 'fixed',
          top: 0,
          left: 0,
          right: 0,
          bottom: 0,
          backgroundColor: 'rgba(0,0,0,0.5)',
          display: 'flex',
          justifyContent: 'center',
          alignItems: 'center',
          zIndex: 1000
        }}>
          <div style={{
            backgroundColor: darkMode.value ? '#444' : 'white',
            padding: '30px',
            borderRadius: '8px',
            maxWidth: '400px',
            width: '90%'
          }}>
            <h3>Modal</h3>
            <p>This is a modal controlled by custom Hook.</p>
            <button onClick={modal.setFalse}>Close</button>
          </div>
        </div>
      )}
    </div>
  );
}

🌐 Network Request Hook

Basic Data Fetching Hook

jsx
// Custom Hook: Data fetching
function useFetch(url, options = {}) {
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  const fetchData = React.useCallback(async () => {
    try {
      setLoading(true);
      setError(null);

      // Simulate network delay
      await new Promise(resolve => setTimeout(resolve, 1000));

      // Simulate API response
      const mockData = {
        '/api/users': [
          { id: 1, name: 'John', email: 'john@example.com' },
          { id: 2, name: 'Jane', email: 'jane@example.com' }
        ],
        '/api/posts': [
          { id: 1, title: 'First Post', content: 'This is the first post content' },
          { id: 2, title: 'Second Post', content: 'This is the second post content' }
        ]
      };

      if (Math.random() > 0.8) {
        throw new Error('Network request failed');
      }

      setData(mockData[url] || { message: 'Data fetched successfully' });
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [url]);

  React.useEffect(() => {
    if (url) {
      fetchData();
    }
  }, [url, fetchData]);

  const refetch = React.useCallback(() => {
    fetchData();
  }, [fetchData]);

  return {
    data,
    loading,
    error,
    refetch
  };
}

// Using data fetching Hook
function DataFetchingExample() {
  const [endpoint, setEndpoint] = React.useState('/api/users');
  const { data, loading, error, refetch } = useFetch(endpoint);

  return (
    <div style={{ padding: '20px' }}>
      <h2>Data Fetching Demo</h2>

      <div style={{ marginBottom: '20px' }}>
        <label>Select endpoint: </label>
        <select value={endpoint} onChange={(e) => setEndpoint(e.target.value)}>
          <option value="/api/users">User List</option>
          <option value="/api/posts">Post List</option>
          <option value="/api/unknown">Unknown Endpoint</option>
        </select>
        <button onClick={refetch} style={{ marginLeft: '10px' }}>
          Refetch
        </button>
      </div>

      {loading && (
        <div style={{ textAlign: 'center', padding: '40px' }}>
          ⏳ Loading...
        </div>
      )}

      {error && (
        <div style={{
          backgroundColor: '#f8d7da',
          color: '#721c24',
          padding: '15px',
          borderRadius: '4px',
          border: '1px solid #f5c6cb'
        }}>
          ❌ Error: {error}
        </div>
      )}

      {data && !loading && (
        <div style={{
          backgroundColor: '#d4edda',
          color: '#155724',
          padding: '15px',
          borderRadius: '4px',
          border: '1px solid #c3e6cb'
        }}>
          <h3>Data Fetched Successfully</h3>
          <pre>{JSON.stringify(data, null, 2)}</pre>
        </div>
      )}
    </div>
  );
}

Advanced Async Hook

jsx
// Custom Hook: Async operations
function useAsync(asyncFunction, immediate = true) {
  const [status, setStatus] = React.useState('idle');
  const [value, setValue] = React.useState(null);
  const [error, setError] = React.useState(null);

  const execute = React.useCallback(async (...args) => {
    setStatus('pending');
    setValue(null);
    setError(null);

    try {
      const response = await asyncFunction(...args);
      setValue(response);
      setStatus('success');
      return response;
    } catch (err) {
      setError(err);
      setStatus('error');
      throw err;
    }
  }, [asyncFunction]);

  React.useEffect(() => {
    if (immediate) {
      execute();
    }
  }, [execute, immediate]);

  return {
    execute,
    status,
    value,
    error,
    isPending: status === 'pending',
    isSuccess: status === 'success',
    isError: status === 'error',
    isIdle: status === 'idle'
  };
}

// Simulate async function
const mockApiCall = async (type) => {
  await new Promise(resolve => setTimeout(resolve, 2000));

  if (type === 'error') {
    throw new Error('Simulated error');
  }

  return {
    type,
    data: `${type} data`,
    timestamp: new Date().toISOString()
  };
};

// Using async Hook
function AsyncExample() {
  const api1 = useAsync(() => mockApiCall('success'), false);
  const api2 = useAsync(() => mockApiCall('error'), false);

  return (
    <div style={{ padding: '20px' }}>
      <h2>Async Operations Demo</h2>

      <div style={{ display: 'flex', gap: '20px' }}>
        <div style={{ flex: 1, border: '1px solid #ddd', padding: '15px', borderRadius: '8px' }}>
          <h3>Success Scenario</h3>
          <button
            onClick={() => api1.execute('success')}
            disabled={api1.isPending}
          >
            {api1.isPending ? 'Requesting...' : 'Send Request'}
          </button>

          {api1.isPending && <div>⏳ Requesting...</div>}
          {api1.isSuccess && (
            <div style={{ color: 'green', marginTop: '10px' }}>
              ✅ Success: {JSON.stringify(api1.value, null, 2)}
            </div>
          )}
          {api1.isError && (
            <div style={{ color: 'red', marginTop: '10px' }}>
              ❌ Error: {api1.error.message}
            </div>
          )}
        </div>

        <div style={{ flex: 1, border: '1px solid #ddd', padding: '15px', borderRadius: '8px' }}>
          <h3>Error Scenario</h3>
          <button
            onClick={() => api2.execute('error')}
            disabled={api2.isPending}
          >
            {api2.isPending ? 'Requesting...' : 'Send Request (will fail)'}
          </button>

          {api2.isPending && <div>⏳ Requesting...</div>}
          {api2.isSuccess && (
            <div style={{ color: 'green', marginTop: '10px' }}>
              ✅ Success: {JSON.stringify(api2.value, null, 2)}
            </div>
          )}
          {api2.isError && (
            <div style={{ color: 'red', marginTop: '10px' }}>
              ❌ Error: {api2.error.message}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

💾 Local Storage Hook

localStorage Hook

jsx
// Custom Hook: Local storage
function useLocalStorage(key, initialValue) {
  // Get initial value
  const [storedValue, setStoredValue] = React.useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error('Error reading localStorage:', error);
      return initialValue;
    }
  });

  // Set value
  const setValue = React.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 setting localStorage:', error);
    }
  }, [key, storedValue]);

  // Remove value
  const removeValue = React.useCallback(() => {
    try {
      window.localStorage.removeItem(key);
      setStoredValue(initialValue);
    } catch (error) {
      console.error('Error removing localStorage:', error);
    }
  }, [key, initialValue]);

  return [storedValue, setValue, removeValue];
}

// Using local storage Hook
function LocalStorageExample() {
  const [name, setName, removeName] = useLocalStorage('user-name', '');
  const [settings, setSettings, removeSettings] = useLocalStorage('user-settings', {
    theme: 'light',
    notifications: true,
    language: 'en-US'
  });
  const [todos, setTodos, removeTodos] = useLocalStorage('todos', []);

  const addTodo = () => {
    const newTodo = {
      id: Date.now(),
      text: `Todo ${todos.length + 1}`,
      completed: false
    };
    setTodos([...todos, newTodo]);
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  const updateSettings = (key, value) => {
    setSettings(prev => ({ ...prev, [key]: value }));
  };

  return (
    <div style={{
      padding: '20px',
      backgroundColor: settings.theme === 'dark' ? '#333' : '#fff',
      color: settings.theme === 'dark' ? '#fff' : '#333',
      minHeight: '100vh'
    }}>
      <h2>Local Storage Demo</h2>

      {/* Username settings */}
      <div style={{ marginBottom: '20px' }}>
        <h3>Username</h3>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="Enter username"
          style={{
            padding: '8px',
            marginRight: '10px',
            backgroundColor: settings.theme === 'dark' ? '#444' : '#fff',
            color: settings.theme === 'dark' ? '#fff' : '#333',
            border: '1px solid #ddd'
          }}
        />
        <button onClick={removeName}>Clear</button>
        {name && <p>Welcome, {name}!</p>}
      </div>

      {/* Settings */}
      <div style={{ marginBottom: '20px' }}>
        <h3>Settings</h3>
        <div style={{ marginBottom: '10px' }}>
          <label>
            Theme:
            <select
              value={settings.theme}
              onChange={(e) => updateSettings('theme', e.target.value)}
              style={{ marginLeft: '10px' }}
            >
              <option value="light">Light</option>
              <option value="dark">Dark</option>
            </select>
          </label>
        </div>
        <div style={{ marginBottom: '10px' }}>
          <label>
            <input
              type="checkbox"
              checked={settings.notifications}
              onChange={(e) => updateSettings('notifications', e.target.checked)}
            />
            Enable notifications
          </label>
        </div>
        <div style={{ marginBottom: '10px' }}>
          <label>
            Language:
            <select
              value={settings.language}
              onChange={(e) => updateSettings('language', e.target.value)}
              style={{ marginLeft: '10px' }}
            >
              <option value="zh-CN">中文</option>
              <option value="en-US">English</option>
            </select>
          </label>
        </div>
        <button onClick={removeSettings}>Reset Settings</button>
      </div>

      {/* Todos */}
      <div>
        <h3>Todos (Auto-saved)</h3>
        <button onClick={addTodo} style={{ marginBottom: '10px' }}>
          Add Todo
        </button>
        <div>
          {todos.map(todo => (
            <div
              key={todo.id}
              style={{
                padding: '8px',
                margin: '4px 0',
                backgroundColor: settings.theme === 'dark' ? '#444' : '#f8f9fa',
                borderRadius: '4px'
              }}
            >
              <label>
                <input
                  type="checkbox"
                  checked={todo.completed}
                  onChange={() => toggleTodo(todo.id)}
                />
                <span style={{
                  textDecoration: todo.completed ? 'line-through' : 'none',
                  marginLeft: '8px'
                }}>
                  {todo.text}
                </span>
              </label>
            </div>
          ))}
        </div>
        {todos.length > 0 && (
          <button onClick={removeTodos} style={{ marginTop: '10px' }}>
            Clear All Todos
          </button>
        )}
      </div>
    </div>
  );
}

⚡ Performance Optimization Hook

Debounce Hook

jsx
// Custom Hook: Debounce
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = React.useState(value);

  React.useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

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

  return debouncedValue;
}

// Custom Hook: Debounced callback
function useDebouncedCallback(callback, delay) {
  const timeoutRef = React.useRef(null);

  const debouncedCallback = React.useCallback((...args) => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    timeoutRef.current = setTimeout(() => {
      callback(...args);
    }, delay);
  }, [callback, delay]);

  React.useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);

  return debouncedCallback;
}

// Debounce demo
function DebounceExample() {
  const [searchTerm, setSearchTerm] = React.useState('');
  const [searchResults, setSearchResults] = React.useState([]);
  const [searching, setSearching] = React.useState(false);

  // Debounced search term
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  // Debounced search function
  const debouncedSearch = useDebouncedCallback((term) => {
    if (term) {
      setSearching(true);
      // Simulate search API
      setTimeout(() => {
        const mockResults = [
          `${term} result 1`,
          `${term} result 2`,
          `${term} result 3`
        ];
        setSearchResults(mockResults);
        setSearching(false);
      }, 300);
    } else {
      setSearchResults([]);
    }
  }, 300);

  // Trigger search with debounced value
  React.useEffect(() => {
    debouncedSearch(debouncedSearchTerm);
  }, [debouncedSearchTerm, debouncedSearch]);

  return (
    <div style={{ padding: '20px' }}>
      <h2>Debounced Search Demo</h2>

      <div style={{ marginBottom: '20px' }}>
        <input
          type="text"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="Enter search..."
          style={{
            padding: '10px',
            width: '300px',
            border: '1px solid #ddd',
            borderRadius: '4px'
          }}
        />
        <div style={{ fontSize: '12px', color: '#666', marginTop: '5px' }}>
          Input triggers search 500ms after stopping
        </div>
      </div>

      {searching && (
        <div style={{ color: '#007bff' }}>
          🔍 Searching...
        </div>
      )}

      {searchResults.length > 0 && (
        <div>
          <h3>Search Results:</h3>
          <ul>
            {searchResults.map((result, index) => (
              <li key={index}>{result}</li>
            ))}
          </ul>
        </div>
      )}

      <div style={{ marginTop: '30px', fontSize: '14px', backgroundColor: '#f8f9fa', padding: '15px', borderRadius: '8px' }}>
        <h4>Real-time Status:</h4>
        <p><strong>Input value:</strong> "{searchTerm}"</p>
        <p><strong>Debounced value:</strong> "{debouncedSearchTerm}"</p>
        <p><strong>Search status:</strong> {searching ? 'Searching' : 'Idle'}</p>
      </div>
    </div>
  );
}

📝 Chapter Summary

Through this chapter, you should have mastered:

Custom Hook Core Concepts

  • ✅ Hook naming conventions (start with "use")
  • ✅ State logic extraction and reuse
  • ✅ Side effect encapsulation and management
  • ✅ Performance optimization techniques

Common Hook Patterns

  • ✅ State management: useCounter, useToggle
  • ✅ Data fetching: useFetch, useAsync
  • ✅ Local storage: useLocalStorage
  • ✅ Performance optimization: useDebounce, useDebouncedCallback

Best Practices

  1. Single responsibility: Each Hook handles only one logic
  2. Dependency optimization: Use useCallback and useMemo reasonably
  3. Error handling: Include complete error handling logic
  4. Resource cleanup: Clean up side effects and subscriptions in a timely manner
  5. Type safety: Use TypeScript to enhance type safety

Continue Learning: Next Chapter - React Styling

Content is for learning and research only.