What's the current pattern for wiring Adobe Analytics/Launch into a React, Next.js, or Angular app?
Load the Adobe Launch embed code once, outside your component tree - in the document head for a client-rendered app, or your framework's document/root template for a server-rendered one - and never call _satellite or the Web SDK from inside a component's render path. Fire a page-view event from a single route-change listener (your router's navigation hook, not every page component individually), and drive everything else through a data layer object components can safely push to at any time, whether or not Launch has finished loading yet.
Why this happens
Adobe Analytics and Launch were built for a document model where a page load equals a page view. A single-page app breaks that assumption on purpose - the whole point of client-side routing is to change the visible content without a full page load - so nothing about Adobe's tooling automatically knows a route changed. Every SPA integration problem traces back to this one gap: something has to explicitly tell Adobe "a new page view just happened," and where you put that call determines whether it's reliable.
The second recurring issue is component lifecycle timing: mounting and unmounting is not the same as loading and unloading a document. A component can mount before the Launch library has finished initializing (see the "_satellite is not defined" pattern), and it can also mount and unmount rapidly during route transitions, firing the same tracking call more than once if the call sits directly in an effect with the wrong dependencies.
Fix it
1. Load the Launch embed code outside React/Angular's control. Next.js: the document/root layout template, not a page component. Plain React (Vite/CRA): index.html. Angular: index.html or a bootstrap-time script. It should load exactly once per full page load, independent of client-side routing.
2. Fire page views from one place: the router's navigation event, not each page's own component.
import { useLocation } from 'react-router-dom';
import { useEffect, useRef } from 'react';
function useAdobePageView() {
const location = useLocation();
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false; // initial load already tracked by the page-bottom rule
return;
}
window.digitalData = window.digitalData || [];
window.digitalData.push({ event: 'virtualPageView', pageName: document.title });
}, [location.pathname]);
}Next.js App Router: do the equivalent in a small client component that watches usePathname(), mounted once near the root layout - not in every page. Angular: subscribe once to Router.events, filtered to NavigationEnd, in the root module.
3. Decide, once, whether the very first load should be a real page view or a virtual one. Launch's own page-bottom rule (Core "DOM Ready" or "Window Loaded" trigger) usually already covers the first load. Your route-change listener should track every navigation after that as a distinct event type (commonly "virtual page view") so the two never double-count the initial load.
4. Keep component code and Launch decoupled through the data layer, not direct calls. A form component pushes an object describing what happened; it never imports or calls _satellite or alloy() directly. That keeps tracking logic testable, keeps it from breaking if Launch hasn't loaded yet, and keeps a component from needing to know which analytics stack is even downstream.
function handleSubmit() {
window.digitalData = window.digitalData || [];
window.digitalData.push({ event: 'leadFormSubmitted', formName: 'contact' });
}5. On the AEP Web SDK specifically: the same rules apply to alloy('sendEvent', ...) as to _satellite.track - it is also asynchronous and also requires the library to be initialized first. Prefer triggering it from a Launch rule reacting to your data-layer push over calling alloy() inline in a component.
How to verify it worked
- Navigate between two routes with the AEP Debugger open and confirm exactly one page-view request fires per navigation - not zero, not two.
- Use the browser's back and forward buttons and repeat the check - client-side routers are the most common place a duplicate or missing virtual page view shows up.
- In Analysis Workspace or CJA's Analysis Workspace, pull a real-time or recent-hits view filtered to your test session and confirm the page name/URL updates on every route change, with no duplicate rows for the same navigation.
Illustrative, not a measured result: a Next.js app that previously fired a page-view push from inside every individual page component might see duplicate virtual page views disappear once that call moves to a single listener mounted at the root layout.
Related
- Why does "_satellite is not defined" happen even though Adobe Launch is installed?
- How do I feed a GTM dataLayer into Adobe Launch?
- What's the reliable way to verify an Adobe Analytics tag fired correctly?