LET'S WORK TOGETHER

React Hooks Deep Dive: Beyond the Basics

12 min readBy Hamza
ReactHooksJavaScriptWeb DevelopmentCustom HooksState Management

React Hooks Deep Dive: Beyond the Basics


React Hooks revolutionized how we write React components, but there's so much more to explore beyond useState and useEffect. Let's dive deep into advanced patterns and techniques that will level up your React skills.


Custom Hooks: The Power of Abstraction


Custom hooks are JavaScript functions whose names start with "use" and that can call other hooks. They're perfect for extracting component logic into reusable functions.


The Anatomy of a Custom Hook


// useLocalStorage.js
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // Get stored value from localStorage or use initialValue
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.log(error);
      return initialValue;
    }
  });

  // Return a wrapped version of useState's setter function
  const setValue = (value) => {
    try {
      // Allow value to be a function
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.log(error);
    }
  };

  return [storedValue, setValue];
}

// Usage in component
function App() {
  const [name, setName] = useLocalStorage('name', 'John');

  return (
    <input
      type="text"
      value={name}
      onChange={(e) => setName(e.target.value)}
    />
  );
}

Advanced Custom Hook Patterns


1. useAsync Hook for Data Fetching

function useAsync(asyncFunction, dependencies = []) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isCancelled = false;

    async function fetchData() {
      try {
        setLoading(true);
        const result = await asyncFunction();

        if (!isCancelled) {
          setData(result);
          setError(null);
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err);
          setData(null);
        }
      } finally {
        if (!isCancelled) {
          setLoading(false);
        }
      }
    }

    fetchData();

    return () => {
      isCancelled = true;
    };
  }, dependencies);

  return { data, loading, error };
}

// Usage
function UserProfile({ userId }) {
  const { data: user, loading, error } = useAsync(
    () => fetchUser(userId),
    [userId]
  );

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

  return <div>{user.name}</div>;
}

2. useDebounce Hook for Performance

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

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

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

  return debouncedValue;
}

// Usage for search input
function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearchTerm) {
      // Make API call here
      searchAPI(debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]);

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

Advanced Hook Patterns


1. useReducer for Complex State


function complexReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, data: action.payload };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    case 'RESET':
      return { loading: false, data: null, error: null };
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

function useComplexApi() {
  const [state, dispatch] = useReducer(complexReducer, {
    loading: false,
    data: null,
    error: null
  });

  const fetchData = async (url) => {
    dispatch({ type: 'FETCH_START' });
    try {
      const response = await fetch(url);
      const data = await response.json();
      dispatch({ type: 'FETCH_SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'FETCH_ERROR', payload: error.message });
    }
  };

  return { ...state, fetchData, reset: () => dispatch({ type: 'RESET' }) };
}

2. useContext for Global State


// ThemeContext.js
import React, { createContext, useContext, useReducer } from 'react';

const ThemeContext = createContext();

function themeReducer(state, action) {
  switch (action.type) {
    case 'TOGGLE_THEME':
      return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
    case 'SET_PRIMARY_COLOR':
      return { ...state, primaryColor: action.payload };
    default:
      return state;
  }
}

export function ThemeProvider({ children }) {
  const [state, dispatch] = useReducer(themeReducer, {
    theme: 'light',
    primaryColor: '#blue'
  });

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

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// Usage
function ThemedComponent() {
  const { theme, primaryColor, dispatch } = useTheme();

  return (
    <div style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
      <button
        onClick={() => dispatch({ type: 'TOGGLE_THEME' })}
        style={{ backgroundColor: primaryColor }}
      >
        Toggle Theme
      </button>
    </div>
  );
}

Performance Optimization


1. useMemo for Expensive Calculations


function ExpensiveComponent({ data, filters }) {
  const filteredAndProcessedData = useMemo(() => {
    return data
      .filter(item => {
        // Complex filtering logic
        return Object.keys(filters).every(key =>
          item[key] === filters[key]
        );
      })
      .map(item => {
        // Complex data transformation
        return {
          ...item,
          computedValue: complexCalculation(item)
        };
      })
      .sort((a, b) => b.computedValue - a.computedValue);
  }, [data, filters]);

  return <DataList data={filteredAndProcessedData} />;
}

2. useCallback for Function Optimization


function TodoList({ todos, onToggle }) {
  const [filter, setFilter] = useState('');

  const handleToggle = useCallback((id) => {
    onToggle(id);
  }, [onToggle]);

  const filteredTodos = useMemo(() => {
    return todos.filter(todo =>
      todo.text.toLowerCase().includes(filter.toLowerCase())
    );
  }, [todos, filter]);

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter todos..."
      />
      {filteredTodos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
        />
      ))}
    </div>
  );
}

Best Practices


  1. Follow the Rules of Hooks: Only call hooks at the top level, only call hooks from React functions
  2. Extract Complex Logic: Create custom hooks for reusable logic
  3. Optimize Performance: Use useMemo and useCallback when necessary
  4. Handle Cleanup: Always cleanup side effects in useEffect
  5. Type Safety: Use TypeScript with hooks for better type safety

Conclusion


React Hooks provide a powerful and flexible way to build React applications. By mastering advanced patterns and custom hooks, you can create more maintainable, reusable, and performant components.


Remember, the key to great hook usage is abstraction and composition. Start simple, and gradually extract more complex logic into custom hooks as your components grow.