DEV Community

Israel Michael
Israel Michael

Posted on • Edited on

Syncing Data Across Browser Tabs in Next.js: A Frontend 'Cron Job' Solution for Timed Fetching

Imagine this. You are building a dynamic Next.js app where data needs to refresh every two minutes. Sounds simple. Drop a setInterval inside useEffect and move on.

That was my assumption too, until I opened multiple browser tabs.

Each tab was fetching data on its own schedule. Nothing was aligned. One tab refreshed, another lagged behind, and the experience felt messy. That was not acceptable.

This post documents how I solved that problem by building a frontend “cron job” that synchronizes data fetching across all open tabs. I will walk through the initial approach, the issue it caused, and the solution that finally made everything click.

Starting out: the timer-based approach

I began with a straightforward setup. Fetch on mount, then fetch every two minutes.

"use client";

import { useEffect, useState } from "react";

export default function Page() {
  const [data, setData] = useState(null);

  const fetchData = async () => {
    const response = await fetch("/api/getData");
    const newData = await response.json();
    setData(newData);
  };

  useEffect(() => {
    fetchData();
    const interval = setInterval(fetchData, 120000);
    return () => clearInterval(interval);
  }, []);

  return <div>{data ? data : "Loading..."}</div>;
}
Enter fullscreen mode Exit fullscreen mode

At first glance, this works perfectly.

The problem: tabs falling out of sync

The issue appeared as soon as I opened a second tab.

Each tab runs its own instance of setInterval, which means timing depends on when the tab was opened.

For example:

  • Open tab A at 12:01. It fetches at 12:03, 12:05, and so on.
  • Open tab B at 12:02. It fetches at 12:04, 12:06, and so on.

Same app, same user, different data states. That inconsistency was the real problem.

What I actually needed was this:

All tabs should fetch at the same exact moments. Ideally at fixed two minute boundaries like 12:00, 12:02, 12:04, no matter when the tab was opened.

The idea: align all fetches to real time

Instead of starting a timer immediately, I decided to align fetching to the clock.

The plan was simple:

  1. Calculate how long until the next even two minute mark.
  2. Wait until that moment using setTimeout.
  3. From there, run a steady two minute interval using setInterval.

This way, every tab locks onto the same schedule.

The final implementation

"use client";

import { useEffect, useState, useCallback } from "react";

export default function Page() {
  const [data, setData] = useState(null);

  // Fetch function wrapped in useCallback to prevent unnecessary re-renders
  const fetchData = useCallback(async () => {
    try {
      const response = await fetch("/api/getData");
      const newData = await response.json();
      setData(newData);
    } catch (err) {
      console.error("Error fetching data:", err);
    }
  }, []);

  useEffect(() => {
    // Fetch immediately on mount so users don't wait up to 2 minutes
    fetchData();

    // Get current time
    const now = new Date();

    // Calculate milliseconds until the next even 2-minute mark (12:00, 12:02, 12:04, etc.)
    // Formula: 120000ms (2 min) - (current position within 2-min cycle)
    const millisecondsUntilNextEvenMinute =
      120000 - 
      ((now.getMinutes() % 2) * 60000 +  // Minutes component
       now.getSeconds() * 1000 +          // Seconds component
       now.getMilliseconds());            // Milliseconds component for precision

    // Variable to store the interval ID so we can clear it on cleanup
    let intervalId;

    // Wait until the next even 2-minute boundary
    const timeoutId = setTimeout(() => {
      // Fetch at the exact 2-minute mark
      fetchData();

      // Now that we're synced, fetch every 2 minutes going forward
      intervalId = setInterval(fetchData, 120000);
    }, millisecondsUntilNextEvenMinute);

    // Cleanup function: clear both timeout and interval to prevent memory leaks
    return () => {
      clearTimeout(timeoutId);
      if (intervalId) clearInterval(intervalId);
    };
  }, [fetchData]); // Include fetchData in dependencies

  return <div>{data ? data : "Loading..."}</div>;
}
Enter fullscreen mode Exit fullscreen mode

What is happening here

  • Initial delay calculation
    I calculate how many milliseconds remain until the next even two minute mark.

  • Delayed first fetch
    setTimeout ensures the first fetch happens exactly on that boundary.

  • Stable interval after sync
    Once aligned, setInterval keeps all tabs fetching together every two minutes.

The result

Now every tab behaves the same way:

  • Open a tab at 12:01:29. It waits until 12:02:00, then fetches every two minutes.
  • Open another tab at 12:01:58. It waits two seconds, then fetches at 12:02:00 alongside the first tab.

Perfect sync, every time.

Final thoughts

This change looks small, but it dramatically improved consistency across tabs. The pattern behaves like a frontend cron job, anchored to real time rather than tab lifecycle.

When this approach makes sense

This is useful when:

  • Data needs to stay consistent across multiple open tabs.
  • Timing matters more than “fetch every X minutes from mount”.
  • A plain setInterval introduces drift you cannot afford.

Sometimes the difference between working code and reliable code is simply aligning with time instead of starting from zero.

Top comments (0)