ReactFeatured

React Hooks Deep Dive: Complete Guide to useState, useEffect, and Custom Hooks

Master React Hooks with this comprehensive guide covering useState, useEffect, useContext, useReducer, and custom hooks. Learn best practices and advanced patterns.

By
reacthooksuseStateuseEffectuseContextuseReducercustom-hooksadvanced

React Hooks Deep Dive: Complete Guide to useState, useEffect, and Custom Hooks

React Hooks revolutionized how we write React components by allowing us to use state and other React features without writing class components. This comprehensive guide will take you through all the essential hooks and teach you how to create powerful custom hooks.

Introduction to React Hooks

Hooks are functions that let you "hook into" React state and lifecycle features from function components. They were introduced in React 16.8 and have become the preferred way to write React components.

Rules of Hooks

Before diving into specific hooks, it's crucial to understand the rules:

  1. Only call hooks at the top level - Don't call hooks inside loops, conditions, or nested functions
  2. Only call hooks from React functions - Call them from React function components or custom hooks
// ❌ Wrong - calling hooks conditionally
function BadExample({ condition }) {
  if (condition) {
    const [state, setState] = useState(0); // This breaks the rules!
  }
  return <div>Bad example</div>;
}

// ✅ Correct - hooks called at top level
function GoodExample({ condition }) {
  const [state, setState] = useState(0);

  if (condition) {
    // Use the state here instead
  }

  return <div>Good example</div>;
}

useState Hook

The useState hook allows you to add state to functional components.

Basic useState Usage

import React, { useState } from 'react';

const CounterExample = () => {
  // Declare state variable with initial value
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
};

State with Objects

const UserProfile = () => {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0,
    preferences: {
      theme: 'light',
      notifications: true,
    },
  });

  const updateName = (newName) => {
    setUser((prevUser) => ({
      ...prevUser,
      name: newName,
    }));
  };

  const updatePreferences = (key, value) => {
    setUser((prevUser) => ({
      ...prevUser,
      preferences: {
        ...prevUser.preferences,
        [key]: value,
      },
    }));
  };

  return (
    <div>
      <input
        type="text"
        value={user.name}
        onChange={(e) => updateName(e.target.value)}
        placeholder="Enter your name"
      />

      <label>
        <input
          type="checkbox"
          checked={user.preferences.notifications}
          onChange={(e) => updatePreferences('notifications', e.target.checked)}
        />
        Enable notifications
      </label>

      <select
        value={user.preferences.theme}
        onChange={(e) => updatePreferences('theme', e.target.value)}
      >
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
    </div>
  );
};

State with Arrays

const TodoList = () => {
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState('');

  const addTodo = () => {
    if (inputValue.trim()) {
      setTodos((prevTodos) => [
        ...prevTodos,
        {
          id: Date.now(),
          text: inputValue,
          completed: false,
        },
      ]);
      setInputValue('');
    }
  };

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

  const deleteTodo = (id) => {
    setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
  };

  return (
    <div>
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
        />
        <button onClick={addTodo}>Add Todo</button>
      </div>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span
              style={{
                textDecoration: todo.completed ? 'line-through' : 'none',
              }}
            >
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

Functional State Updates

const Counter = () => {
  const [count, setCount] = useState(0);

  // ❌ This might not work as expected in rapid clicks
  const incrementWrong = () => {
    setCount(count + 1);
    setCount(count + 1); // This won't increment by 2!
  };

  // ✅ Use functional updates for predictable behavior
  const incrementCorrect = () => {
    setCount((prevCount) => prevCount + 1);
    setCount((prevCount) => prevCount + 1); // This will increment by 2
  };

  const incrementAsync = () => {
    setTimeout(() => {
      // Always use functional update in async operations
      setCount((prevCount) => prevCount + 1);
    }, 1000);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementCorrect}>Increment by 2</button>
      <button onClick={incrementAsync}>Increment after 1s</button>
    </div>
  );
};

useEffect Hook

The useEffect hook lets you perform side effects in function components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount combined.

Basic useEffect Usage

import React, { useState, useEffect } from 'react';

const BasicEffect = () => {
  const [count, setCount] = useState(0);

  // Effect runs after every render
  useEffect(() => {
    document.title = `Count: ${count}`;
  });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

useEffect with Dependencies

const DataFetcher = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // Effect runs only when userId changes
  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error('Failed to fetch user');
        }
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    if (userId) {
      fetchUser();
    }
  }, [userId]); // Dependency array

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
    </div>
  );
};

Cleanup with useEffect

const Timer = () => {
  const [seconds, setSeconds] = useState(0);
  const [isActive, setIsActive] = useState(false);

  useEffect(() => {
    let interval = null;

    if (isActive) {
      interval = setInterval(() => {
        setSeconds((prevSeconds) => prevSeconds + 1);
      }, 1000);
    }

    // Cleanup function
    return () => {
      if (interval) {
        clearInterval(interval);
      }
    };
  }, [isActive]); // Re-run effect when isActive changes

  const toggle = () => setIsActive(!isActive);
  const reset = () => {
    setSeconds(0);
    setIsActive(false);
  };

  return (
    <div>
      <p>Timer: {seconds}s</p>
      <button onClick={toggle}>{isActive ? 'Pause' : 'Start'}</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
};

Multiple useEffect Hooks

const UserDashboard = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [notifications, setNotifications] = useState([]);

  // Effect for fetching user data
  useEffect(() => {
    const fetchUser = async () => {
      const response = await fetch(`/api/users/${userId}`);
      const userData = await response.json();
      setUser(userData);
    };

    fetchUser();
  }, [userId]);

  // Effect for fetching user posts
  useEffect(() => {
    const fetchPosts = async () => {
      const response = await fetch(`/api/users/${userId}/posts`);
      const postsData = await response.json();
      setPosts(postsData);
    };

    fetchPosts();
  }, [userId]);

  // Effect for setting up notifications WebSocket
  useEffect(() => {
    const ws = new WebSocket(`ws://localhost:8080/notifications/${userId}`);

    ws.onmessage = (event) => {
      const notification = JSON.parse(event.data);
      setNotifications((prev) => [...prev, notification]);
    };

    return () => {
      ws.close();
    };
  }, [userId]);

  // Effect for document title
  useEffect(() => {
    if (user) {
      document.title = `Dashboard - ${user.name}`;
    }

    return () => {
      document.title = 'App';
    };
  }, [user]);

  return (
    <div>
      {user && (
        <div>
          <h1>Welcome, {user.name}!</h1>
          <div>Posts: {posts.length}</div>
          <div>Notifications: {notifications.length}</div>
        </div>
      )}
    </div>
  );
};

useContext Hook

The useContext hook allows you to consume context values without wrapping components in Consumer components.

Creating and Using Context

import React, { createContext, useContext, useState } from 'react';

// Create context
const ThemeContext = createContext();
const UserContext = createContext();

// Theme Provider Component
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  const value = {
    theme,
    toggleTheme,
  };

  return (
    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
  );
};

// User Provider Component
const UserProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const login = async (credentials) => {
    // Simulate API call
    const userData = await authenticateUser(credentials);
    setUser(userData);
    setIsAuthenticated(true);
  };

  const logout = () => {
    setUser(null);
    setIsAuthenticated(false);
  };

  const value = {
    user,
    isAuthenticated,
    login,
    logout,
  };

  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};

// Custom hooks for consuming context
const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
};

const useUser = () => {
  const context = useContext(UserContext);
  if (!context) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
};

// Components using context
const Header = () => {
  const { theme, toggleTheme } = useTheme();
  const { user, isAuthenticated, logout } = useUser();

  return (
    <header className={`header ${theme}`}>
      <h1>My App</h1>
      <div>
        <button onClick={toggleTheme}>
          Switch to {theme === 'light' ? 'dark' : 'light'} mode
        </button>
        {isAuthenticated ? (
          <div>
            <span>Welcome, {user.name}!</span>
            <button onClick={logout}>Logout</button>
          </div>
        ) : (
          <button>Login</button>
        )}
      </div>
    </header>
  );
};

const App = () => {
  return (
    <ThemeProvider>
      <UserProvider>
        <div className="app">
          <Header />
          {/* Other components */}
        </div>
      </UserProvider>
    </ThemeProvider>
  );
};

Complex Context with useReducer

import React, { createContext, useContext, useReducer } from 'react';

// Shopping Cart Context
const CartContext = createContext();

// Cart actions
const CART_ACTIONS = {
  ADD_ITEM: 'ADD_ITEM',
  REMOVE_ITEM: 'REMOVE_ITEM',
  UPDATE_QUANTITY: 'UPDATE_QUANTITY',
  CLEAR_CART: 'CLEAR_CART',
  APPLY_DISCOUNT: 'APPLY_DISCOUNT',
};

// Cart reducer
const cartReducer = (state, action) => {
  switch (action.type) {
    case CART_ACTIONS.ADD_ITEM:
      const existingItem = state.items.find(
        (item) => item.id === action.payload.id
      );

      if (existingItem) {
        return {
          ...state,
          items: state.items.map((item) =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          ),
        };
      } else {
        return {
          ...state,
          items: [...state.items, { ...action.payload, quantity: 1 }],
        };
      }

    case CART_ACTIONS.REMOVE_ITEM:
      return {
        ...state,
        items: state.items.filter((item) => item.id !== action.payload),
      };

    case CART_ACTIONS.UPDATE_QUANTITY:
      return {
        ...state,
        items: state.items.map((item) =>
          item.id === action.payload.id
            ? { ...item, quantity: action.payload.quantity }
            : item
        ),
      };

    case CART_ACTIONS.CLEAR_CART:
      return {
        ...state,
        items: [],
      };

    case CART_ACTIONS.APPLY_DISCOUNT:
      return {
        ...state,
        discount: action.payload,
      };

    default:
      return state;
  }
};

// Initial state
const initialCartState = {
  items: [],
  discount: 0,
};

// Cart Provider
const CartProvider = ({ children }) => {
  const [state, dispatch] = useReducer(cartReducer, initialCartState);

  // Action creators
  const addItem = (item) => {
    dispatch({ type: CART_ACTIONS.ADD_ITEM, payload: item });
  };

  const removeItem = (itemId) => {
    dispatch({ type: CART_ACTIONS.REMOVE_ITEM, payload: itemId });
  };

  const updateQuantity = (itemId, quantity) => {
    dispatch({
      type: CART_ACTIONS.UPDATE_QUANTITY,
      payload: { id: itemId, quantity },
    });
  };

  const clearCart = () => {
    dispatch({ type: CART_ACTIONS.CLEAR_CART });
  };

  const applyDiscount = (discount) => {
    dispatch({ type: CART_ACTIONS.APPLY_DISCOUNT, payload: discount });
  };

  // Computed values
  const totalItems = state.items.reduce(
    (total, item) => total + item.quantity,
    0
  );
  const subtotal = state.items.reduce(
    (total, item) => total + item.price * item.quantity,
    0
  );
  const total = subtotal - (subtotal * state.discount) / 100;

  const value = {
    ...state,
    addItem,
    removeItem,
    updateQuantity,
    clearCart,
    applyDiscount,
    totalItems,
    subtotal,
    total,
  };

  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
};

// Custom hook for using cart
const useCart = () => {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error('useCart must be used within a CartProvider');
  }
  return context;
};

// Example usage
const ProductCard = ({ product }) => {
  const { addItem } = useCart();

  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => addItem(product)}>Add to Cart</button>
    </div>
  );
};

const CartSummary = () => {
  const { items, totalItems, total, clearCart } = useCart();

  return (
    <div className="cart-summary">
      <h3>Cart ({totalItems} items)</h3>
      {items.map((item) => (
        <div key={item.id}>
          {item.name} x {item.quantity} = ${item.price * item.quantity}
        </div>
      ))}
      <div>Total: ${total.toFixed(2)}</div>
      <button onClick={clearCart}>Clear Cart</button>
    </div>
  );
};

useReducer Hook

The useReducer hook is usually preferable to useState when you have complex state logic or when the next state depends on the previous one.

Basic useReducer Example

import React, { useReducer } from 'react';

// Define action types
const ACTION_TYPES = {
  INCREMENT: 'increment',
  DECREMENT: 'decrement',
  RESET: 'reset',
  SET_VALUE: 'set_value',
};

// Reducer function
const counterReducer = (state, action) => {
  switch (action.type) {
    case ACTION_TYPES.INCREMENT:
      return { count: state.count + (action.payload || 1) };
    case ACTION_TYPES.DECREMENT:
      return { count: state.count - (action.payload || 1) };
    case ACTION_TYPES.RESET:
      return { count: 0 };
    case ACTION_TYPES.SET_VALUE:
      return { count: action.payload };
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
};

const Counter = () => {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: ACTION_TYPES.INCREMENT })}>
        +1
      </button>
      <button
        onClick={() => dispatch({ type: ACTION_TYPES.INCREMENT, payload: 5 })}
      >
        +5
      </button>
      <button onClick={() => dispatch({ type: ACTION_TYPES.DECREMENT })}>
        -1
      </button>
      <button onClick={() => dispatch({ type: ACTION_TYPES.RESET })}>
        Reset
      </button>
      <button
        onClick={() => dispatch({ type: ACTION_TYPES.SET_VALUE, payload: 100 })}
      >
        Set to 100
      </button>
    </div>
  );
};

Complex Form with useReducer

const FORM_ACTIONS = {
  UPDATE_FIELD: 'UPDATE_FIELD',
  SET_ERRORS: 'SET_ERRORS',
  SET_LOADING: 'SET_LOADING',
  RESET_FORM: 'RESET_FORM',
  SUBMIT_SUCCESS: 'SUBMIT_SUCCESS',
  SUBMIT_ERROR: 'SUBMIT_ERROR',
};

const formReducer = (state, action) => {
  switch (action.type) {
    case FORM_ACTIONS.UPDATE_FIELD:
      return {
        ...state,
        values: {
          ...state.values,
          [action.field]: action.value,
        },
        errors: {
          ...state.errors,
          [action.field]: '', // Clear field error when user types
        },
      };

    case FORM_ACTIONS.SET_ERRORS:
      return {
        ...state,
        errors: action.errors,
      };

    case FORM_ACTIONS.SET_LOADING:
      return {
        ...state,
        isLoading: action.isLoading,
      };

    case FORM_ACTIONS.RESET_FORM:
      return {
        ...state,
        values: action.initialValues || state.initialValues,
        errors: {},
        isLoading: false,
      };

    case FORM_ACTIONS.SUBMIT_SUCCESS:
      return {
        ...state,
        isLoading: false,
        errors: {},
        submitCount: state.submitCount + 1,
      };

    case FORM_ACTIONS.SUBMIT_ERROR:
      return {
        ...state,
        isLoading: false,
        errors: action.errors,
      };

    default:
      return state;
  }
};

const ContactForm = () => {
  const initialValues = {
    name: '',
    email: '',
    subject: '',
    message: '',
  };

  const [state, dispatch] = useReducer(formReducer, {
    values: initialValues,
    initialValues,
    errors: {},
    isLoading: false,
    submitCount: 0,
  });

  const updateField = (field, value) => {
    dispatch({
      type: FORM_ACTIONS.UPDATE_FIELD,
      field,
      value,
    });
  };

  const validateForm = () => {
    const errors = {};

    if (!state.values.name.trim()) {
      errors.name = 'Name is required';
    }

    if (!state.values.email.trim()) {
      errors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(state.values.email)) {
      errors.email = 'Email is invalid';
    }

    if (!state.values.subject.trim()) {
      errors.subject = 'Subject is required';
    }

    if (!state.values.message.trim()) {
      errors.message = 'Message is required';
    }

    return errors;
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    const errors = validateForm();

    if (Object.keys(errors).length > 0) {
      dispatch({ type: FORM_ACTIONS.SET_ERRORS, errors });
      return;
    }

    dispatch({ type: FORM_ACTIONS.SET_LOADING, isLoading: true });

    try {
      // Simulate API call
      await new Promise((resolve) => setTimeout(resolve, 2000));

      dispatch({ type: FORM_ACTIONS.SUBMIT_SUCCESS });
      alert('Form submitted successfully!');

      // Reset form after successful submission
      dispatch({ type: FORM_ACTIONS.RESET_FORM });
    } catch (error) {
      dispatch({
        type: FORM_ACTIONS.SUBMIT_ERROR,
        errors: { submit: 'Failed to submit form. Please try again.' },
      });
    }
  };

  const resetForm = () => {
    dispatch({ type: FORM_ACTIONS.RESET_FORM });
  };

  return (
    <form onSubmit={handleSubmit} className="contact-form">
      <h2>Contact Us</h2>

      <div className="form-group">
        <label htmlFor="name">Name:</label>
        <input
          type="text"
          id="name"
          value={state.values.name}
          onChange={(e) => updateField('name', e.target.value)}
          className={state.errors.name ? 'error' : ''}
        />
        {state.errors.name && (
          <span className="error-text">{state.errors.name}</span>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          value={state.values.email}
          onChange={(e) => updateField('email', e.target.value)}
          className={state.errors.email ? 'error' : ''}
        />
        {state.errors.email && (
          <span className="error-text">{state.errors.email}</span>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="subject">Subject:</label>
        <input
          type="text"
          id="subject"
          value={state.values.subject}
          onChange={(e) => updateField('subject', e.target.value)}
          className={state.errors.subject ? 'error' : ''}
        />
        {state.errors.subject && (
          <span className="error-text">{state.errors.subject}</span>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="message">Message:</label>
        <textarea
          id="message"
          rows="5"
          value={state.values.message}
          onChange={(e) => updateField('message', e.target.value)}
          className={state.errors.message ? 'error' : ''}
        />
        {state.errors.message && (
          <span className="error-text">{state.errors.message}</span>
        )}
      </div>

      {state.errors.submit && (
        <div className="error-text">{state.errors.submit}</div>
      )}

      <div className="form-actions">
        <button type="submit" disabled={state.isLoading}>
          {state.isLoading ? 'Submitting...' : 'Submit'}
        </button>
        <button type="button" onClick={resetForm}>
          Reset
        </button>
      </div>

      <div className="form-info">Submit count: {state.submitCount}</div>
    </form>
  );
};

Custom Hooks

Custom hooks allow you to extract component logic into reusable functions. They are JavaScript functions that start with "use" and can call other hooks.

Simple Custom Hooks

// useToggle hook
const useToggle = (initialValue = false) => {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => setValue((prev) => !prev), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);

  return [value, toggle, setTrue, setFalse];
};

// useCounter hook
const useCounter = (initialValue = 0, step = 1) => {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => setCount((prev) => prev + step), [step]);
  const decrement = useCallback(() => setCount((prev) => prev - step), [step]);
  const reset = useCallback(() => setCount(initialValue), [initialValue]);
  const setValue = useCallback((value) => setCount(value), []);

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

// useLocalStorage hook
const useLocalStorage = (key, initialValue) => {
  // Get value from localStorage or use initial value
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  // Return a wrapped version of useState's setter function that persists the new value to localStorage
  const setValue = 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 key "${key}":`, error);
      }
    },
    [key, storedValue]
  );

  return [storedValue, setValue];
};

// Usage examples
const ExampleComponent = () => {
  const [isVisible, toggleVisible, showElement, hideElement] = useToggle(false);
  const { count, increment, decrement, reset } = useCounter(0, 2);
  const [name, setName] = useLocalStorage('userName', '');

  return (
    <div>
      <button onClick={toggleVisible}>{isVisible ? 'Hide' : 'Show'}</button>
      {isVisible && <p>This element is toggleable!</p>}

      <div>
        <p>Count: {count}</p>
        <button onClick={increment}>+2</button>
        <button onClick={decrement}>-2</button>
        <button onClick={reset}>Reset</button>
      </div>

      <div>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="Enter your name"
        />
        <p>Stored name: {name}</p>
      </div>
    </div>
  );
};

Advanced Custom Hooks

// useFetch hook for data fetching
const useFetch = (url, options = {}) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

    try {
      const response = await fetch(url, options);

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [url, JSON.stringify(options)]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

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

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

// useDebounce hook
const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

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

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

  return debouncedValue;
};

// useWindowSize hook
const useWindowSize = () => {
  const [windowSize, setWindowSize] = useState({
    width: undefined,
    height: undefined,
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener('resize', handleResize);
    handleResize(); // Call handler right away so state gets updated with initial window size

    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return windowSize;
};

// useIntersectionObserver hook
const useIntersectionObserver = (elementRef, options = {}) => {
  const [isIntersecting, setIsIntersecting] = useState(false);

  useEffect(() => {
    const element = elementRef.current;

    if (!element) return;

    const observer = new IntersectionObserver(([entry]) => {
      setIsIntersecting(entry.isIntersecting);
    }, options);

    observer.observe(element);

    return () => {
      observer.unobserve(element);
    };
  }, [elementRef, options]);

  return isIntersecting;
};

// Complex custom hook: useForm
const useForm = (initialValues, validationRules = {}) => {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const setValue = useCallback(
    (name, value) => {
      setValues((prev) => ({ ...prev, [name]: value }));

      // Clear error when user starts typing
      if (errors[name]) {
        setErrors((prev) => ({ ...prev, [name]: '' }));
      }
    },
    [errors]
  );

  const setFieldTouched = useCallback((name) => {
    setTouched((prev) => ({ ...prev, [name]: true }));
  }, []);

  const validate = useCallback(() => {
    const newErrors = {};

    Object.keys(validationRules).forEach((field) => {
      const rules = validationRules[field];
      const value = values[field];

      for (const rule of rules) {
        const error = rule(value, values);
        if (error) {
          newErrors[field] = error;
          break; // Stop at first error for this field
        }
      }
    });

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  }, [values, validationRules]);

  const handleSubmit = useCallback(
    (onSubmit) => {
      return async (e) => {
        e.preventDefault();

        // Mark all fields as touched
        const allTouched = Object.keys(values).reduce((acc, key) => {
          acc[key] = true;
          return acc;
        }, {});
        setTouched(allTouched);

        if (!validate()) return;

        setIsSubmitting(true);

        try {
          await onSubmit(values);
        } catch (error) {
          console.error('Form submission error:', error);
        } finally {
          setIsSubmitting(false);
        }
      };
    },
    [values, validate]
  );

  const resetForm = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
    setIsSubmitting(false);
  }, [initialValues]);

  const getFieldProps = useCallback(
    (name) => ({
      value: values[name] || '',
      onChange: (e) => setValue(name, e.target.value),
      onBlur: () => setFieldTouched(name),
      error: touched[name] && errors[name],
    }),
    [values, errors, touched, setValue, setFieldTouched]
  );

  return {
    values,
    errors,
    touched,
    isSubmitting,
    setValue,
    setFieldTouched,
    handleSubmit,
    resetForm,
    getFieldProps,
    isValid: Object.keys(errors).length === 0,
  };
};

// Validation rules
const required =
  (message = 'This field is required') =>
  (value) => {
    if (!value || !value.toString().trim()) {
      return message;
    }
  };

const email =
  (message = 'Please enter a valid email') =>
  (value) => {
    if (value && !/\S+@\S+\.\S+/.test(value)) {
      return message;
    }
  };

const minLength = (min, message) => (value) => {
  if (value && value.length < min) {
    return message || `Must be at least ${min} characters`;
  }
};

// Usage of advanced custom hooks
const AdvancedExample = () => {
  const { data: users, loading, error, refetch } = useFetch('/api/users');
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 300);
  const windowSize = useWindowSize();
  const lazyElementRef = useRef();
  const isElementVisible = useIntersectionObserver(lazyElementRef, {
    threshold: 0.1,
  });

  const form = useForm(
    { name: '', email: '', message: '' },
    {
      name: [required()],
      email: [required(), email()],
      message: [required(), minLength(10)],
    }
  );

  const handleFormSubmit = async (values) => {
    console.log('Submitting:', values);
    await new Promise((resolve) => setTimeout(resolve, 1000));
    alert('Form submitted successfully!');
    form.resetForm();
  };

  // Effect for debounced search
  useEffect(() => {
    if (debouncedSearchTerm) {
      console.log('Searching for:', debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]);

  return (
    <div>
      <div>
        <h3>Window Size</h3>
        <p>
          Width: {windowSize.width}px, Height: {windowSize.height}px
        </p>
      </div>

      <div>
        <h3>Search</h3>
        <input
          type="text"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="Search users..."
        />
        <p>Debounced: {debouncedSearchTerm}</p>
      </div>

      <div>
        <h3>Users</h3>
        {loading && <p>Loading...</p>}
        {error && <p>Error: {error}</p>}
        {data && (
          <div>
            <button onClick={refetch}>Refresh</button>
            <ul>
              {users.slice(0, 5).map((user) => (
                <li key={user.id}>{user.name}</li>
              ))}
            </ul>
          </div>
        )}
      </div>

      <form onSubmit={form.handleSubmit(handleFormSubmit)}>
        <h3>Contact Form</h3>
        <div>
          <input {...form.getFieldProps('name')} placeholder="Name" />
          {form.getFieldProps('name').error && (
            <span className="error">{form.getFieldProps('name').error}</span>
          )}
        </div>
        <div>
          <input {...form.getFieldProps('email')} placeholder="Email" />
          {form.getFieldProps('email').error && (
            <span className="error">{form.getFieldProps('email').error}</span>
          )}
        </div>
        <div>
          <textarea {...form.getFieldProps('message')} placeholder="Message" />
          {form.getFieldProps('message').error && (
            <span className="error">{form.getFieldProps('message').error}</span>
          )}
        </div>
        <button type="submit" disabled={form.isSubmitting || !form.isValid}>
          {form.isSubmitting ? 'Submitting...' : 'Submit'}
        </button>
      </form>

      <div style={{ height: '1000px', background: '#f0f0f0', padding: '20px' }}>
        <p>Scroll down to see the lazy element...</p>
      </div>

      <div
        ref={lazyElementRef}
        style={{ height: '200px', background: '#e0e0e0' }}
      >
        <h3>Lazy Element</h3>
        <p>Is visible: {isElementVisible ? 'Yes' : 'No'}</p>
      </div>
    </div>
  );
};

Best Practices and Common Patterns

Optimizing Hook Performance

import React, { useState, useEffect, useCallback, useMemo } from 'react';

const OptimizedComponent = ({ items, category, onItemSelect }) => {
  const [searchTerm, setSearchTerm] = useState('');
  const [sortOrder, setSortOrder] = useState('asc');

  // Memoize expensive calculations
  const filteredAndSortedItems = useMemo(() => {
    const filtered = items.filter(
      (item) =>
        item.category === category &&
        item.name.toLowerCase().includes(searchTerm.toLowerCase())
    );

    return filtered.sort((a, b) => {
      const multiplier = sortOrder === 'asc' ? 1 : -1;
      return a.name.localeCompare(b.name) * multiplier;
    });
  }, [items, category, searchTerm, sortOrder]);

  // Memoize event handlers to prevent unnecessary re-renders
  const handleSearchChange = useCallback((e) => {
    setSearchTerm(e.target.value);
  }, []);

  const handleSortChange = useCallback((newSortOrder) => {
    setSortOrder(newSortOrder);
  }, []);

  const handleItemClick = useCallback(
    (item) => {
      onItemSelect(item);
    },
    [onItemSelect]
  );

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={handleSearchChange}
        placeholder="Search items..."
      />

      <button onClick={() => handleSortChange('asc')}>Sort A-Z</button>
      <button onClick={() => handleSortChange('desc')}>Sort Z-A</button>

      <ul>
        {filteredAndSortedItems.map((item) => (
          <li key={item.id} onClick={() => handleItemClick(item)}>
            {item.name}
          </li>
        ))}
      </ul>
    </div>
  );
};

Error Boundaries with Hooks

import React, { useState, useEffect } from 'react';

// Custom hook for error handling
const useErrorHandler = () => {
  const [error, setError] = useState(null);

  const resetError = () => setError(null);

  const handleError = (error) => {
    setError(error);
    console.error('Error caught by error handler:', error);
  };

  return { error, resetError, handleError };
};

// Component with error handling
const DataComponent = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const { error, resetError, handleError } = useErrorHandler();

  const fetchData = async () => {
    setLoading(true);
    resetError();

    try {
      const response = await fetch('/api/data');
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const result = await response.json();
      setData(result);
    } catch (err) {
      handleError(err);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  if (error) {
    return (
      <div className="error-container">
        <h3>Something went wrong</h3>
        <p>{error.message}</p>
        <button onClick={fetchData}>Try Again</button>
      </div>
    );
  }

  if (loading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      {data ? (
        <pre>{JSON.stringify(data, null, 2)}</pre>
      ) : (
        <p>No data available</p>
      )}
    </div>
  );
};

Conclusion

React Hooks have revolutionized how we write React components, making it easier to share logic between components and manage state in functional components. Understanding these core hooks and how to create custom hooks will significantly improve your React development skills.

Key Takeaways

  1. useState: Perfect for local component state management
  2. useEffect: Handle side effects and lifecycle events
  3. useContext: Share state across component trees without prop drilling
  4. useReducer: Manage complex state logic with predictable updates
  5. Custom Hooks: Extract and reuse component logic across multiple components

Best Practices

  • Always follow the Rules of Hooks
  • Use the dependency array in useEffect wisely
  • Memoize expensive calculations with useMemo
  • Memoize event handlers with useCallback
  • Create custom hooks for reusable logic
  • Keep components small and focused
  • Use TypeScript for better type safety

Next Steps

Now that you've mastered React Hooks, consider exploring:

  • Advanced React Patterns: Render props, compound components, higher-order components
  • State Management Libraries: Redux Toolkit, Zustand, Jotai
  • Testing Hooks: React Testing Library, Jest
  • Performance Optimization: React.memo, React Profiler
  • Concurrent Features: Suspense, useDeferredValue, useTransition

React Hooks provide a powerful and elegant way to manage state and side effects in functional components. With practice and understanding of these patterns, you'll be able to build robust, maintainable React applications!