Zum Inhalt springen

Custom React Hooks for API Calls + Lazy Loading View-Based Data

In this post, I’ll walk you through a problem I encountered during a real-world Next.js project — multiple API-bound sections loading all at once, even before the user scrolled to them.

This kind of eager data fetching doesn’t just waste network requests — it hurts performance, increases TTI, and slows down Core Web Vitals.

To fix it, I built a custom React hook that delays API calls until a component enters the viewport. Think of it as useEffect + IntersectionObserver bundled neatly for reuse.

📍 The Problem

In one of my recent projects (a performance-optimized energy website built with Next.js + GraphQL), we had:

  • 4–5 API-heavy sections on a single page
  • All triggering data fetch on initial load, regardless of visibility
  • Resulting in unnecessary bandwidth usage and slow perceived performance

🛠️ The Solution: Build a Reusable Hook

The goal was simple:

Don’t fetch anything until the section is actually visible.

Let’s break it down.

🔁 Step 1 – useInView Hook

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

export const useInView = () => {
  const ref = useRef(null);
  const [isInView, setIsInView] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) setIsInView(true);
    });

    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  return [ref, isInView];
};

⚙️ Step 2 – Use It in a Component

const ServicesSection = () => {
  const [ref, isInView] = useInView();
  const [data, setData] = useState(null);

  useEffect(() => {
    if (isInView && !data) {
      fetch('/api/services')
        .then((res) => res.json())
        .then(setData);
    }
  }, [isInView]);

  return (
    <section ref={ref}>
      <h2>Our Services</h2>
      {data ? data.map(item => <p key={item.id}>{item.title}</p>) : <p>Loading...</p>}
    </section>
  );
};

♻️ Step 3 – Abstract It into useLazyFetchOnView

export const useLazyFetchOnView = (callback) => {
  const [ref, isInView] = useInView();
  const [hasFetched, setHasFetched] = useState(false);

  useEffect(() => {
    if (isInView && !hasFetched) {
      callback();
      setHasFetched(true);
    }
  }, [isInView]);

  return ref;
};

Now your component looks much cleaner:

const ref = useLazyFetchOnView(() => {
  fetch('/api/testimonials')
    .then(res => res.json())
    .then(setTestimonials);
});

🔄 Integration with React Query / Zustand

You can replace fetch() with your existing state logic:

const { refetch } = useQuery('teamData', fetchTeam, { enabled: false });

const ref = useLazyFetchOnView(() => refetch());

Or dispatch a Zustand action:

const fetchData = useStore((state) => state.fetchData);
const ref = useLazyFetchOnView(fetchData);

📈 Before vs After

Metric Before After
API Calls 6 3
LCP 4.3s 2.1s
Lighthouse Score 66 93+
TTI 5.2s 2.8s

This small pattern helped us reduce noise, clean up component logic, and improve performance across the board.

🧠 Final Thoughts

Lazy loading is not just for images.

If you have view-based components — testimonials, blogs, services, etc. — don’t fetch data until they come into view.

This pattern can be reused across your app, and it’s easy to test and maintain.

Let me know if you’d like a reusable package out of it — happy to open source it if there’s interest.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert