REACT Lesson 16 – Custom Hooks | Dataplexa
LESSON 16

Custom Hooks

Build reusable hooks that extract component logic and share stateful behavior across the DataFlow dashboard.

Custom hooks transform chaos into order. You copy the same state logic between three components. Or pass props down seven levels just to share one function. React's built-in hooks like useState solve common problems. But your app has unique problems. That's where custom hooks shine. A custom hook is just a JavaScript function that starts with "use" and calls other hooks inside. Think of it like creating your own Swiss Army knife — combining basic tools into something perfectly shaped for your specific needs. The DataFlow dashboard has several components that need similar functionality. The StatsBar fetches revenue data. The ChartSection fetches chart data. Both need loading states, error handling, and data refresh logic. Without custom hooks, you'd copy this logic everywhere. With them, you write once and reuse everywhere.

What Makes a Hook Custom

React hooks follow strict rules. Custom hooks inherit these same rules but add flexibility. You can combine multiple built-in hooks. You can add your own logic. You can return whatever makes sense for your use case. The naming convention matters. Functions starting with "use" tell React's linter this is a hook. It will enforce the Rules of Hooks — only call hooks at the top level, only call hooks inside React functions. Break the naming rule and you break React's safety guarantees.
// This IS a custom hook - starts with "use"
function useCounter() {
  const [count, setCount] = useState(0);
  
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  
  return { count, increment, decrement };
}

// This is NOT a custom hook - wrong name
function counterLogic() {
  const [count, setCount] = useState(0); // This will break!
  return { count };
}
DataFlow Counter Hook

What just happened?

The useCounter hook encapsulates state and methods. Any component can use it by calling the hook and destructuring the return value. The hook runs on every render, maintaining its own state independently in each component that uses it.

Building a Data Fetching Hook

The DataFlow dashboard fetches data from multiple endpoints. Revenue stats, user metrics, order data — each needs loading states, error handling, and retry logic. Perfect candidate for a custom hook. A useFetch hook can handle this pattern. Pass it a URL, get back data, loading state, and error state. Clean, reusable, testable.
// DataFlow's data fetching hook
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // Simulate API call for DataFlow
    setLoading(true);
    setError(null);
    
    setTimeout(() => {
      if (url.includes('revenue')) {
        setData({ revenue: '$94,245', growth: '+12.3%' });
      } else {
        setData({ users: '1,847', active: '892' });
      }
      setLoading(false);
    }, 1000);
  }, [url]);
  
  return { data, loading, error };
}
DataFlow Fetch Hook

What just happened?

Two components use the same useFetch hook with different URLs. Each gets independent state management. The hook handles loading, success, and error states automatically. Try refreshing — both cards load independently.

Hook Dependencies and Performance

Custom hooks follow the same dependency rules as useEffect. When you call other hooks inside, you need to consider what triggers re-runs. The hook above re-runs when the URL changes because of [url] in the dependency array. But what if you want to add a refresh function? Or cache results? Custom hooks can get sophisticated quickly. The key is keeping them focused. One concern per hook works better than kitchen-sink utilities.
// Enhanced DataFlow fetch hook with refresh
function useDataFlowFetch(endpoint) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  
  const fetchData = useCallback(() => {
    setLoading(true);
    // Simulate different DataFlow endpoints
    setTimeout(() => {
      const responses = {
        revenue: { value: '$127,834', change: '+8.2%' },
        orders: { value: '2,491', change: '+15.7%' },
        users: { value: '8,234', change: '+4.1%' }
      };
      setData(responses[endpoint] || responses.revenue);
      setLoading(false);
    }, 800);
  }, [endpoint]);
  
  useEffect(() => {
    fetchData();
  }, [fetchData]);
  
  return { data, loading, refresh: fetchData };
}
DataFlow Enhanced Fetch

What just happened?

The enhanced hook returns a refresh function alongside data and loading state. Each card can refresh independently by calling this function. The useCallback prevents infinite re-renders by memoizing the fetch function.

When to Build Custom Hooks

Not every piece of shared logic needs a custom hook. Sometimes a regular function works better. Custom hooks shine when you need to share stateful logic between components. Three key indicators tell you to build one. First, you're copying useState and useEffect patterns between components. The DataFlow dashboard might have multiple charts that all need similar data fetching. Extract that pattern into a hook. Second, you have complex state logic that multiple components need. Maybe you're building a multi-step form across different pages. A useFormState hook could manage current step, validation, and navigation. Third, you want to hide implementation details from components. Your hook might combine multiple API calls, cache results, and handle retries. Components just see clean data and loading states.

Custom Hook Benefits

Reusability across components, easier testing in isolation, cleaner component code, and consistent patterns across your app. The DataFlow team can build a hook library that every developer on the project can use.

Local Storage Hook

The DataFlow dashboard needs to remember user preferences. Dark mode toggle, selected date range, favorite widgets — all should persist between sessions. A useLocalStorage hook can handle this pattern elegantly.
// DataFlow localStorage hook for user preferences
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = localStorage.getItem(`dataflow_${key}`);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.log(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });
  
  const setValue = (value) => {
    try {
      setStoredValue(value);
      localStorage.setItem(`dataflow_${key}`, JSON.stringify(value));
    } catch (error) {
      console.log(`Error setting localStorage key "${key}":`, error);
    }
  };
  
  return [storedValue, setValue];
}
DataFlow Preferences

What just happened?

The hook reads from localStorage on first render and writes on every change. Try switching the theme or changing settings, then refresh the page — your preferences persist. The hook handles JSON serialization and error cases automatically.

Testing Custom Hooks

Custom hooks are functions. Test them like functions. React provides testing utilities that make this straightforward. You can test hooks in isolation without rendering full components. The key insight is that hooks need a React context to run. You can't just call useLocalStorage() in a test like a regular function. But you can use React Testing Library's renderHook utility. This makes hook testing much simpler than testing components. No DOM concerns, no event simulation — just pure logic testing. Your DataFlow hooks become predictable, reliable building blocks.

Hook Testing Strategy

Test the hook logic separately from component rendering. Mock external dependencies like localStorage or API calls. Focus on state changes and return values rather than DOM effects. This gives you confidence in your hook behavior.

Custom hooks represent React's compositional power. You're not limited to useState and useEffect. Build exactly what your app needs. The DataFlow dashboard might need useAuth, useNotifications, useBilling, useReports — each encapsulating complex logic behind simple interfaces. Start small. Extract obvious patterns first. As your hook library grows, you'll see opportunities to combine hooks or split complex ones. The goal isn't clever abstraction — it's maintainable code that makes your team more productive.

Quiz

1. What makes a function a valid custom hook in the DataFlow dashboard?


2. When multiple DataFlow components use the same custom hook, what happens to the state?


3. Why would you wrap a function in useCallback inside a DataFlow custom hook?


Up Next: Context API

Share state across the entire DataFlow component tree without prop drilling through every level.