# Mastering React Hooks: A Complete Guide with Real-World Examples

Have you ever started a new web project with React and had to struggle with how to handle things such as state and lifecycle methods?

React provides Hooks which are a more direct API to the React concepts you already know: props, state, context, refs, and lifecycle. Every time I work on a web project with React, I notice how much I need to use one or more of these hooks to build a robust web app.

I'll use this article to show how you can use the most popular React hooks in a real-world example. If you follow this article to the very end, you'll have learnt how to use hooks such as useState, useEffect, useMemo, useCallback, useContext, useRef and custom hooks.

## What are React Hooks?

React Hooks are functions that let you "hook into" React features from function components. They allow you to use state and other React features without writing a class component. Hooks were introduced in React 16.8 and have revolutionized how we write React applications.

## 1\. useState - Managing Component State

The `useState` hook is the most fundamental hook for managing state in functional components.

```jsx
import React, { useState } from 'react';

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      
      <input 
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter your name"
      />
    </div>
  );
}
```

### **Real-world Example: Search and Filter State**

```javascript
function TransactionPage() {
  const [searchTerm, setSearchTerm] = useState('');
  const [filterObj, setFilterObj] = useState({
    amount: undefined,
    dateFrom: undefined,
    dateTo: undefined,
    status: '',
    locations: [],
  });

  return (
    <div>
      <input 
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search transactions..."
      />
    </div>
  );
}
```

## **2\. useEffect - Handling Side Effects**

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

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

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // Effect runs after every render
  useEffect(() => {
    fetchUser(userId)
      .then(userData => {
        setUser(userData);
        setLoading(false);
      });
  }, [userId]); // Dependency array - effect runs when userId changes

  // Cleanup effect
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Timer tick');
    }, 1000);

    return () => clearInterval(timer); // Cleanup function
  }, []);

  if (loading) return <div>Loading...</div>;
  return <div>Welcome, {user?.name}!</div>;
}
```

### **Real-world Example: Data Fetching and Cleanup**

```javascript
function TransactionList() {
  const [transactions, setTransactions] = useState([]);
  const [totalCount, setTotalCount] = useState(0);

  // Fetch data when component mounts or dependencies change
  useEffect(() => {
    const fetchTransactions = async () => {
      try {
        const response = await api.getTransactions({
          pageNumber: 1,
          pageSize: 10,
        });
        setTransactions(response.data.results);
        setTotalCount(response.data.totalCount);
      } catch (error) {
        console.error('Failed to fetch transactions:', error);
      }
    };

    fetchTransactions();
  }, []); // Empty dependency array - runs once on mount

  // Clear search when component unmounts
  useEffect(() => {
    return () => {
      setSearchTerm('');
    };
  }, []);

  return (
    <div>
      {transactions.map(transaction => (
        <div key={transaction.id}>{transaction.amount}</div>
      ))}
    </div>
  );
}
```

## **3\. useMemo - Optimizing Expensive Calculations**

The useMemo hook memoizes expensive calculations and only recalculates when dependencies change.

```javascript
import React, { useState, useMemo } from 'react';

function ExpensiveComponent({ items, filter }) {
  const [count, setCount] = useState(0);

  // Expensive calculation - only runs when items or filter changes
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  // Another expensive calculation
  const totalValue = useMemo(() => {
    return filteredItems.reduce((sum, item) => sum + item.value, 0);
  }, [filteredItems]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Total Value: ${totalValue}</p>
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}: ${item.value}</li>
        ))}
      </ul>
    </div>
  );
}
```

### **Real-world Example: Filter Status Calculation**

```javascript
function TerminalManager() {
  const [appliedFilters, setAppliedFilters] = useState([]);

  // Memoized calculations for filter states
  const statusActive = useMemo(
    () => !!appliedFilters?.find(el => el.key === "status")?.key,
    [appliedFilters]
  );

  const dateActive = useMemo(
    () => !!appliedFilters?.find(el => el.key === "date")?.key,
    [appliedFilters]
  );

  const terminalAddressActive = useMemo(
    () => !!appliedFilters?.find(el => el.key === "terminalAddress")?.key,
    [appliedFilters]
  );

  return (
    <div>
      <FilterTab active={statusActive} title="Status" />
      <FilterTab active={dateActive} title="Date" />
      <FilterTab active={terminalAddressActive} title="Address" />
    </div>
  );
}
```

## **4\. useCallback - Memoizing Functions**

The useCallback hook memoizes functions to prevent unnecessary re-renders of child components.

```javascript
import React, { useState, useCallback, memo } from 'react';

// Child component that only re-renders when props change
const ChildComponent = memo(({ onButtonClick, name }) => {
  console.log('Child component rendered');
  return (
    <div>
      <p>Hello, {name}!</p>
      <button onClick={onButtonClick}>Click me</button>
    </div>
  );
});

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

  // Without useCallback, this function is recreated on every render
  const handleButtonClick = useCallback(() => {
    console.log('Button clicked!');
    // Some expensive operation
  }, []); // Empty dependency array - function never changes

  // With dependencies
  const handleNameChange = useCallback((newName) => {
    setName(newName);
  }, []);

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

### **Real-world Example: Event Handlers**

```javascript
function SearchComponent({ onSearch, suggestions }) {
  const [searchTerm, setSearchTerm] = useState('');

  // Memoized search handler
  const handleSearch = useCallback((term) => {
    if (term.trim()) {
      onSearch(term);
    }
  }, [onSearch]);

  // Memoized input change handler
  const handleInputChange = useCallback((e) => {
    const value = e.target.value;
    setSearchTerm(value);
    
    // Debounced search
    const timeoutId = setTimeout(() => {
      handleSearch(value);
    }, 300);

    return () => clearTimeout(timeoutId);
  }, [handleSearch]);

  return (
    <input
      value={searchTerm}
      onChange={handleInputChange}
      placeholder="Search..."
    />
  );
}
```

## **5\. useContext - Sharing State Across Components**

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

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

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

// Context provider component
function AppProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);

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

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

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

// Components using context
function Header() {
  const { theme, setTheme } = useTheme();
  const { user } = useUser();

  return (
    <header className={`header ${theme}`}>
      <h1>Welcome, {user?.name || 'Guest'}!</h1>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </header>
  );
}

function App() {
  return (
    <AppProvider>
      <Header />
      {/* Other components */}
    </AppProvider>
  );
}
```

### **Real-world Example: Global State Management**

```javascript
// Store context for global state
const StoreContext = createContext();

function StoreProvider({ children }) {
  const [amount, setAmount] = useState(undefined);
  const [period, setPeriod] = useState(undefined);
  const [downloadHistory, setDownloadHistory] = useState([]);

  const value = {
    amount,
    setAmount,
    period,
    setPeriod,
    downloadHistory,
    setDownloadHistory,
  };

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

// Custom hook to use store
function useStore() {
  const context = useContext(StoreContext);
  if (!context) {
    throw new Error('useStore must be used within StoreProvider');
  }
  return context;
}

// Component using global state
function TransactionFilter() {
  const { amount, setAmount, period, setPeriod } = useStore();

  return (
    <div>
      <input
        type="number"
        value={amount || ''}
        onChange={(e) => setAmount(e.target.value)}
        placeholder="Enter amount"
      />
      <select value={period || ''} onChange={(e) => setPeriod(e.target.value)}>
        <option value="">Select period</option>
        <option value="today">Today</option>
        <option value="week">This Week</option>
        <option value="month">This Month</option>
      </select>
    </div>
  );
}
```

## **6\. useRef - Accessing DOM Elements and Persisting Values**

The useRef hook creates a mutable ref object that persists across re-renders.

```javascript
import React, { useRef, useEffect, useState } from 'react';

function FocusInput() {
  const inputRef = useRef(null);
  const countRef = useRef(0);
  const [renderCount, setRenderCount] = useState(0);

  useEffect(() => {
    // Focus input on mount
    inputRef.current?.focus();
  }, []);

  useEffect(() => {
    // Update ref value without causing re-render
    countRef.current = countRef.current + 1;
    console.log('Render count:', countRef.current);
  });

  const handleFocus = () => {
    inputRef.current?.focus();
  };

  return (
    <div>
      <input ref={inputRef} placeholder="This will be focused" />
      <button onClick={handleFocus}>Focus Input</button>
      <button onClick={() => setRenderCount(renderCount + 1)}>
        Re-render ({renderCount})
      </button>
      <p>Component has rendered {countRef.current} times</p>
    </div>
  );
}
```

### **Real-world Example: Click Outside Detection**

```javascript
function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };

    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);

    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef(null);

  useOnClickOutside(modalRef, onClose);

  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div ref={modalRef} className="modal-content">
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </div>
  );
}
```

## **7\. Custom Hooks - Reusable Logic**

Custom hooks allow you to extract component logic into reusable functions.

```javascript
// Custom hook for API calls
function useApi(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        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);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

// Custom hook for local storage
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 key "${key}":`, error);
      return initialValue;
    }
  });

  const setValue = useCallback((value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  }, [key]);

  return [storedValue, setValue];
}

// Using custom hooks
function UserProfile({ userId }) {
  const { data: user, loading, error } = useApi(`/api/users/${userId}`);
  const [preferences, setPreferences] = useLocalStorage('userPreferences', {});

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

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <button onClick={() => setPreferences({ ...preferences, theme: 'dark' })}>
        Set Dark Theme
      </button>
    </div>
  );
}
```

### **Real-world Example: Window Dimensions Hook**  

```javascript
function useWindowDimensions() {
  const [windowDimensions, setWindowDimensions] = useState({
    width: undefined,
    height: undefined,
  });

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

    handleResize(); // Set initial dimensions
    window.addEventListener('resize', handleResize);

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

  return windowDimensions;
}

// Usage
function ResponsiveComponent() {
  const { width } = useWindowDimensions();

  return (
    <div>
      {width < 768 ? (
        <MobileLayout />
      ) : (
        <DesktopLayout />
      )}
    </div>
  );
}
```

## **Best Practices and Common Pitfalls**

### **1\. Rules of Hooks**

* Only call hooks at the top level of your React function
    
* Don't call hooks inside loops, conditions, or nested functions
    
* Only call hooks from React function components or custom hooks
    

### **2\. Dependency Arrays**

```javascript
// ❌ Missing dependencies
useEffect(() => {
  fetchData(userId, filter);
}, []); // Missing userId and filter

// ✅ Correct dependencies
useEffect(() => {
  fetchData(userId, filter);
}, [userId, filter]);

// ✅ Using useCallback for stable references
const fetchData = useCallback(async (id, filterValue) => {
  // fetch logic
}, []);

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

### **3\. Avoiding Infinite Loops**

```javascript
// ❌ This will cause infinite re-renders
useEffect(() => {
  setData(processData(data));
}, [data]);

// ✅ Use a different trigger or memoization
useEffect(() => {
  setData(processData(rawData));
}, [rawData]);

// ✅ Or use useMemo for derived state
const processedData = useMemo(() => {
  return processData(rawData);
}, [rawData]);
```

### **4\. Performance Optimization**

```javascript
// ❌ Creating new objects/arrays in render
function Component({ items }) {
  return (
    <ExpensiveChild 
      items={items.filter(item => item.active)} // New array every render
      config={{ theme: 'dark' }} // New object every render
    />
  );
}

// ✅ Use useMemo and useCallback
function Component({ items }) {
  const activeItems = useMemo(
    () => items.filter(item => item.active),
    [items]
  );
  
  const config = useMemo(() => ({ theme: 'dark' }), []);

  return <ExpensiveChild items={activeItems} config={config} />;
}
```

## **Conclusion**

React Hooks have revolutionized how we write React applications by providing a more direct and functional approach to managing state and side effects. The hooks we've covered - useState, useEffect, useMemo, useCallback, useContext, useRef and custom hooks - form the foundation of modern React development.

Key takeaways:

* **useState** for managing component state
    
* **useEffect** for side effects and lifecycle events
    
* **useMemo** for expensive calculations
    
* **useCallback** for memoizing functions
    
* **useContext** for sharing state across components
    
* **useRef** for DOM access and persistent values
    
* **Custom hooks** for reusable logic
    

By mastering these hooks and following best practices, you'll be able to build more efficient, maintainable, and performant React applications. Remember to always consider the dependency arrays, avoid common pitfalls like infinite loops, and use performance optimization techniques when necessary.

Start incorporating these patterns into your React projects, and you'll quickly see how hooks can simplify your code while making it more powerful and reusable.

---

*Happy coding! 🚀*
