Skip to content
whovisited.

Next.js

Prevent custom events from racing an async tracker in Next.js

An async analytics script can load after a signup or form succeeds. Use Next.js Script readiness to queue a small number of custom events without blocking the user flow.

Why a successful action can happen before analytics is ready

A user submits a signup form, your server creates the account, and your success handler calls `window.whovisited.track("signup_completed")`. The signup succeeds, but no conversion event appears. One cause is timing: an asynchronous analytics script can still be downloading when the application code reaches the success handler.

Next.js recommends `next/script` for third-party scripts such as analytics. With the `afterInteractive` strategy, the script loads after some hydration has occurred. The browser's `async` script attribute also means the browser executes the script when its download is ready, not at a fixed point relative to unrelated application work. Until the tracker runs, `window.whovisited` does not exist.

Optional chaining avoids a JavaScript exception, but `window.whovisited?.track(...)` silently does nothing when the global is missing. That can be a sensible fail-open choice for the signup experience, but it is not a reliable event-delivery strategy. Queueing a small number of allowed events in memory can bridge this initialization race.

Add a bounded in-memory queue in a Client Component

Next.js Script's `onReady` callback runs after the script loads and again when the component mounts after client-side navigation. Event handlers belong in a Client Component, so keep the queue and the Script component together. This example holds at most 20 synthetic event descriptions in the current page's memory; it does not write them to browser storage.

tsx
// app/components/tracker-loader.tsx
"use client";

import Script from "next/script";

type QueuedEvent = {
  name: string;
  properties?: Record<string, string | number | boolean | null>;
};

declare global {
  interface Window {
    whovisited?: {
      track: (name: string, properties?: QueuedEvent["properties"]) => void;
    };
  }
}

const pending: QueuedEvent[] = [];
const maxPending = 20;

function flushPendingEvents() {
  const tracker = window.whovisited;
  if (!tracker) return;
  for (const event of pending.splice(0)) {
    tracker.track(event.name, event.properties);
  }
}

export function trackWhenReady(event: QueuedEvent) {
  if (typeof window === "undefined") return false;
  const tracker = window.whovisited;
  if (tracker) {
    flushPendingEvents();
    tracker.track(event.name, event.properties);
    return true;
  }
  if (pending.length >= maxPending) return false;
  pending.push(event);
  return true;
}

export function TrackerLoader({ siteId }: { siteId: string }) {
  return (
    <Script
      src="https://whovisited.net/tracker.js"
      data-site-id={siteId}
      strategy="afterInteractive"
      onReady={flushPendingEvents}
      onError={() => { pending.length = 0; }}
    />
  );
}

Render `<TrackerLoader siteId="YOUR_SITE_ID" />` from the root layout, and import `trackWhenReady` into the client code that handles the business action. The helper drains earlier queued items before sending a later event, so a success event does not jump ahead of events that were waiting for initialization. If the script fails to load, the example discards its pending list when Next.js reports the error.

Queue a confirmed outcome, not a button click

Call the helper after the server confirms the action you want to measure. A click is an interaction; it does not establish that an account was created or a form was accepted. For example, the client submit handler can call the tracker helper after receiving a successful response from your existing signup endpoint:

tsx
const result = await createAccount(formData);
if (result.ok) {
  trackWhenReady({ name: "signup_completed" });
  router.push("/welcome");
}

Use an event name that matches the goal you configured in WhoVisited. Custom event names are case-sensitive, begin with a letter, and may contain letters, digits, or underscores up to 64 characters. Do not send names, email addresses, passwords, form contents, access tokens, or account IDs as properties. The goal should describe an outcome that your application actually confirmed.

If the queue is already full, `trackWhenReady` returns `false`; ignore that result for the business flow. Analytics must never decide whether the user sees a successful signup screen. Keep duplicate-submit prevention and account creation idempotency in your application, because a queued event is not a substitute for either.

Test both the timing fix and its failure path

  1. In a test environment, throttle the browser's network in DevTools and submit a synthetic signup before the tracker script finishes loading. Confirm that the signup proceeds and the custom event is queued in memory.
  2. Restore normal network conditions and confirm that the tracker initializes and sends the event. In the Network panel, inspect the bootstrap request and the later events request; a script download alone does not prove ingestion.
  3. After the event appears in the visitor journey, check that the configured custom-event goal uses the exact event name and that the same session contains the conversion.
  4. Block the tracker request or use a test page with a failing script URL. Confirm that the signup still succeeds, pending events are discarded on load error, and no analytics code throws in the application.
  5. If consent is required, repeat with consent withheld and granted. Confirm that the queue does not replay actions from before consent and that the consent manager remains responsible for the decision.

For installation and a complete conversion walkthrough, see the [Next.js signup tracking guide](https://whovisited.net/blog/nextjs-signup-conversion-tracking). For an event that still does not appear, follow the [tracker debugging checklist](https://whovisited.net/blog/analytics-tracker-loads-but-no-events). Next.js's [Script component reference](https://nextjs.org/docs/app/api-reference/components/script) documents `afterInteractive`, `onReady`, and the Client Component restriction for event handlers.

References and further reading

  1. Next.js: Script Component
  2. Next.js: How to load and optimize scripts
  3. WhoVisited: Track completed signups in a Next.js app