What are React Hooks?

React Hooks are special functions that let you "hook into" React features from functional components. They allow you to use state and other React features without writing class components, making your code simpler, more reusable, and easier to test.

🎯 Why Hooks Were Introduced

  • Simpler Code: No need for class components in most cases
  • Reusable Logic: Extract stateful logic into custom hooks
  • Better Performance: Easier optimization and no class binding issues
  • Cleaner Side Effects: Better organization of component lifecycle logic
Class Component (Old Way)
Class Component
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      name: ''
    };
  }

  componentDidMount() {
    document.title = `Count: ${this.state.count}`;
  }

  componentDidUpdate() {
    document.title = `Count: ${this.state.count}`;
  }

  incrementCount = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.incrementCount}>
          Increment
        </button>
        <input
          value={this.state.name}
          onChange={(e) => this.setState({name: e.target.value})}
        />
      </div>
    );
  }
}
Functional Component with Hooks
Hooks Component
import { useState, useEffect } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // Combines componentDidMount and componentDidUpdate
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  const incrementCount = () => {
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementCount}>
        Increment
      </button>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
    </div>
  );
}

useState Hook

The useState hook is the most commonly used hook. It allows you to add state to functional components. It returns an array with two elements: the current state value and a function to update it.

🔧 useState Syntax

useState Examples
import { useState } from 'react';

function StateExamples() {
  // Basic state
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  const [isVisible, setIsVisible] = useState(false);
  
  // Array state
  const [items, setItems] = useState([]);
  
  // Object state
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });

  // Function to update count
  const increment = () => {
    setCount(count + 1);
    // Or use functional update for safety
    setCount(prevCount => prevCount + 1);
  };

  // Adding item to array
  const addItem = (newItem) => {
    setItems(prevItems => [...prevItems, newItem]);
  };

  // Updating object state
  const updateUser = (field, value) => {
    setUser(prevUser => ({
      ...prevUser,
      [field]: value
    }));
  };

  return (
    <div>
      <h3>Count: {count}</h3>
      <button onClick={increment}>Increment</button>
      
      <input 
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter name"
      />
      
      <div>
        <label>
          <input 
            type="checkbox"
            checked={isVisible}
            onChange={(e) => setIsVisible(e.target.checked)}
          />
          Show content
        </label>
        {isVisible && <p>This content is visible!</p>}
      </div>
    </div>
  );
}

useEffect Hook

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

🔄 useEffect Patterns

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

function EffectExamples() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState(null);
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  // 1. Effect that runs on every render
  useEffect(() => {
    console.log('Component rendered');
  });

  // 2. Effect that runs only once (on mount)
  useEffect(() => {
    console.log('Component mounted');
    document.title = 'My React App';
  }, []); // Empty dependency array

  // 3. Effect that runs when count changes
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]); // Dependency array with count

  // 4. Effect with cleanup (like componentWillUnmount)
  useEffect(() => {
    const handleResize = () => {
      setWindowWidth(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    // Cleanup function
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Runs once, cleanup on unmount

  // 5. Data fetching effect
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('/api/data');
        const result = await response.json();
        setData(result);
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();
  }, []); // Fetch once on mount

  // 6. Effect with multiple dependencies
  useEffect(() => {
    if (count > 0 && data) {
      console.log('Both count and data are available');
    }
  }, [count, data]); // Runs when either count or data changes

  return (
    <div>
      <h3>Count: {count}</h3>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      
      <p>Window width: {windowWidth}px</p>
      
      {data ? (
        <div>Data loaded: {JSON.stringify(data)}</div>
      ) : (
        <div>Loading data...</div>
      )}
    </div>
  );
}

useContext Hook

The useContext hook allows you to subscribe to React context without introducing nesting. It's perfect for sharing data across multiple components without prop drilling.

🌐 Context Pattern

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

// 1. Create Context
const ThemeContext = createContext();
const UserContext = createContext();

// 2. Create Provider Component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

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

  const value = {
    theme,
    toggleTheme
  };

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

function UserProvider({ children }) {
  const [user, setUser] = useState({
    name: 'John Doe',
    email: 'john@example.com'
  });

  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}

// 3. Components that use Context
function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  const { user } = useContext(UserContext);

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

function Profile() {
  const { user, setUser } = useContext(UserContext);
  const { theme } = useContext(ThemeContext);

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

  return (
    <div className={`profile ${theme}`}>
      <h2>Profile</h2>
      <p>Name: {user.name}</p>
      <p>Email: {user.email}</p>
      <input 
        value={user.name}
        onChange={(e) => updateName(e.target.value)}
      />
    </div>
  );
}

// 4. Main App with Providers
function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <Header />
        <Profile />
      </UserProvider>
    </ThemeProvider>
  );
}

Custom Hooks

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

🔧 Custom Hook Examples

Custom Hooks
import { useState, useEffect } from 'react';

// 1. useCounter - Reusable counter logic
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount(prev => prev + 1);
  const decrement = () => setCount(prev => prev - 1);
  const reset = () => setCount(initialValue);

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

// 2. useFetch - Reusable data fetching
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('Network response was not ok');
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    if (url) {
      fetchData();
    }
  }, [url]);

  return { data, loading, error };
}

// 3. useLocalStorage - Persist state in localStorage
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error('Error reading localStorage:', error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error('Error setting localStorage:', error);
    }
  };

  return [storedValue, setValue];
}

// 4. useToggle - Simple toggle functionality
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = () => setValue(prev => !prev);
  const setTrue = () => setValue(true);
  const setFalse = () => setValue(false);

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

// Using custom hooks in components
function CounterComponent() {
  const { count, increment, decrement, reset } = useCounter(10);

  return (
    <div>
      <h3>Count: {count}</h3>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

function DataComponent() {
  const { data, loading, error } = useFetch('/api/users');

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

  return (
    <div>
      {data && data.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

function PreferencesComponent() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [isVisible, { toggle }] = useToggle(false);

  return (
    <div>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Current theme: {theme}
      </button>
      
      <button onClick={toggle}>
        {isVisible ? 'Hide' : 'Show'} Content
      </button>
      
      {isVisible && <p>This content is toggleable!</p>}
    </div>
  );
}

Rules of Hooks

React hooks have specific rules that must be followed to ensure they work correctly. These rules are enforced by the ESLint plugin for React hooks.

📋 Hook Rules

  1. Only call hooks at the top level - Never inside loops, conditions, or nested functions
  2. Only call hooks from React functions - From functional components or custom hooks
  3. Custom hooks must start with "use" - This tells React it's a hook

✅ Correct Usage

Good Examples
function GoodComponent() {
  // ✅ Always at top level
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  useEffect(() => {
    // ✅ Effect logic here
  }, []);

  // ✅ Custom hook call
  const { data } = useFetch('/api/data');

  return <div>{/* JSX */}</div>;
}

❌ Incorrect Usage

Bad Examples
function BadComponent() {
  // ❌ Don't call hooks conditionally
  if (someCondition) {
    const [count, setCount] = useState(0);
  }

  // ❌ Don't call hooks in loops
  for (let i = 0; i < 10; i++) {
    useEffect(() => {}, []);
  }

  // ❌ Don't call hooks in event handlers
  const handleClick = () => {
    const [data, setData] = useState(null);
  };

  return <div>{/* JSX */}</div>;
}

🔧 ESLint Plugin

Install the React hooks ESLint plugin to catch hook rule violations:

ESLint Setup
npm install eslint-plugin-react-hooks

// .eslintrc.js
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

Advanced Hook Patterns

🚀 useReducer for Complex State

useReducer Example
import { useReducer } from 'react';

// Reducer function
const todoReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, {
          id: Date.now(),
          text: action.payload,
          completed: false
        }]
      };
    
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };
    
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      };
    
    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload
      };
    
    default:
      return state;
  }
};

function TodoApp() {
  const initialState = {
    todos: [],
    filter: 'all'
  };

  const [state, dispatch] = useReducer(todoReducer, initialState);

  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 });
  };

  return (
    <div>
      <TodoInput onAdd={addTodo} />
      <TodoList 
        todos={state.todos} 
        onToggle={toggleTodo}
        onDelete={deleteTodo}
      />
    </div>
  );
}

⚡ Performance Optimization Hooks

useMemo and useCallback
import { useState, useMemo, useCallback } from 'react';

function OptimizedComponent({ items }) {
  const [filter, setFilter] = useState('');
  const [sortBy, setSortBy] = useState('name');

  // useMemo for expensive calculations
  const filteredAndSortedItems = useMemo(() => {
    console.log('Filtering and sorting items...');
    
    let filtered = items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
    
    return filtered.sort((a, b) => {
      if (sortBy === 'name') {
        return a.name.localeCompare(b.name);
      } else if (sortBy === 'price') {
        return a.price - b.price;
      }
      return 0;
    });
  }, [items, filter, sortBy]); // Only recalculate when these change

  // useCallback for stable function references
  const handleItemClick = useCallback((itemId) => {
    console.log('Item clicked:', itemId);
    // Do something with the item
  }, []); // Function doesn't depend on any values

  const handleFilterChange = useCallback((newFilter) => {
    setFilter(newFilter);
  }, []);

  return (
    <div>
      <SearchInput 
        value={filter}
        onChange={handleFilterChange}
      />
      
      <SortSelector 
        value={sortBy}
        onChange={setSortBy}
      />
      
      <ItemList 
        items={filteredAndSortedItems}
        onItemClick={handleItemClick}
      />
    </div>
  );
}

Hook Best Practices

✅ Do's

  • Always follow the Rules of Hooks
  • Use custom hooks to share logic
  • Keep effects focused and specific
  • Use dependency arrays correctly
  • Extract complex state logic to useReducer
  • Use ESLint plugin for hook validation

❌ Don'ts

  • Don't call hooks conditionally
  • Don't forget cleanup in useEffect
  • Don't omit dependencies
  • Don't use hooks in regular functions
  • Don't over-optimize with useMemo/useCallback
  • Don't nest hooks inside loops

🎯 When to Use Each Hook

  • useState: Simple state management
  • useEffect: Side effects and lifecycle
  • useContext: Avoiding prop drilling
  • useReducer: Complex state logic
  • useMemo: Expensive calculations
  • useCallback: Stable function references

🔄 Common Patterns

  • Data fetching with loading states
  • Form handling with validation
  • Timer and interval management
  • Event listener cleanup
  • Local storage persistence
  • Component communication

What's Next?

You now have a solid understanding of React Hooks and how they can make your functional components more powerful and reusable. Next, you'll learn about managing complex application state.

🚀 Continue Learning

  1. State Management - Context API, Redux, and global state patterns
  2. Routing - Building single-page applications with React Router
  3. Development Tools - Essential tools for React development
  4. Transition Guide - Complete migration strategies