Webclat logoWebclat . | Amplitude Solutions

QA - framework setup

What's the correct setup sequence to get Amplitude tracking events inside a React, React Native, or Expo app?

The fix, why it happens, and how to confirm it actually worked in your own instance.

The fix, in short

Amplitude needs one init call made once, at your app's true entry point, using the package built for your platform - @amplitude/analytics-browser for React, @amplitude/analytics-react-native for React Native and Expo (never the browser package in a native app) - with identify() and setUserId() called immediately after a user logs in, not buried inside a component that might mount more than once.

Why this happens

The single most common failure across all three platforms is the same mistake wearing different clothes: init() gets called from inside a component (a top-level App component, a layout, a screen) instead of a module that loads exactly once. Components remount on hot reload, on navigation in some router configurations, and twice under React 18 StrictMode in development - each remount can re-run init(), which either duplicates events or silently drops the second instance's tracking depending on the SDK version.

React Native and Expo have a second, platform-specific trap: reaching for the browser SDK package because the API looks identical. The browser package assumes `window`, `document`, and browser storage APIs exist. In a React Native runtime none of those exist, so it either throws immediately on import or silently no-ops every call depending on how the bundler resolves the missing globals - there is no in-between working state.

Expo specifically adds a build-config layer: the React Native Amplitude SDK's native dependencies (used for things like device ID persistence via native storage) need to actually be present in the compiled binary, which for a managed Expo workflow means an EAS development build or prebuild - a plain Expo Go session run without a custom dev client silently lacks native modules that were added after Expo Go was already installed.

Fix it

React (web)

  1. Install `@amplitude/analytics-browser` (not `amplitude-js` for new projects - it's the newer SDK). Create one file, e.g. `lib/analytics.ts`, that calls `amplitude.init(process.env.NEXT_PUBLIC_AMPLITUDE_KEY, { defaultTracking: true })` at module scope, and import that file exactly once, at your app's root (`main.tsx`/`App.tsx` or root layout).
  2. Never call `amplitude.init` again anywhere else, including inside a custom hook that multiple components might use - hooks re-run per component instance, init should not.
  3. On login, call `amplitude.setUserId(user.id)` and, if you have profile properties worth tracking, an `Identify` object via `amplitude.identify(identifyObj)` right after authentication resolves - not inside a `useEffect` with a dependency array that could re-fire on unrelated re-renders.
  4. On logout, call `amplitude.reset()` before the next user logs in on the same browser/device, so the next session doesn't inherit the previous user's identity.

React Native / Expo

  1. Install `@amplitude/analytics-react-native` specifically - confirm your package.json does not also have `@amplitude/analytics-browser` anywhere in the dependency tree (a copy-pasted web example is the usual source of that mistake).
  2. Call init once, in your app's root entry file (`App.tsx` or the root `_layout.tsx` in Expo Router), guarded so it only runs on first load: a module-level `let initialized = false` flag, or simply keeping the init call at module scope outside any component function, both work.
  3. If you're on Expo's managed workflow, confirm you're running a development build (`npx expo run:ios` / `run:android`, or an EAS development build installed on-device) and not the generic Expo Go app - Expo Go only bundles the native modules that shipped with that Expo Go release, so a newer native-dependent package like the Amplitude SDK's storage layer may be silently absent.
  4. Call `setUserId`/`identify` after your auth flow resolves, exactly as in the web variant, and call `amplitude.reset()` on logout so a shared device doesn't merge two users' histories.

How to verify it worked

  1. Web: open devtools Network tab, filter for `api.amplitude.com`, trigger an action you instrumented, and confirm exactly one request fires per action - not zero, not two.
  2. React Native/Expo: since there's no browser devtools Network tab on-device, use Flipper's Network plugin, React Native's built-in debugger network inspector, or a proxy tool (e.g. a local mitm proxy) to confirm the same outbound request to Amplitude's ingestion endpoint.
  3. In Amplitude, use User Look-Up with the device ID or user ID you tested with (visible via `amplitude.getDeviceId()` / `getUserId()` in a debug console log) and confirm the events you just fired show up against that exact identity, not merged into a different one.
  4. Force a hot reload (web) or a fast refresh (React Native) after your instrumentation is live, then repeat an instrumented action - if you see duplicate events for the same action, init is still running more than once and the module-scope guard needs a second look.

Related: Why does Amplitude intermittently throw "Invalid apiKey" or a runQueuedFunctions error? · Why does Amplitude's device ID regenerate on every session/tab instead of persisting? · Why doesn't the Amplitude snippet load with Next.js's Script component, or why do events go missing right after a redirect?

Frequently asked questions

Can I use the same Amplitude project for both my web app and my mobile app?+

Yes - Amplitude projects are platform-agnostic; what matters is that both platforms send a consistent identity (the same user_id at login) so a user's web and mobile activity merge into one profile instead of two. Use separate API keys per project only if you deliberately want to keep the datasets apart, not by default.

Do I need a different setup for Next.js App Router versus a plain React SPA?+

The core init pattern is identical, but Next.js App Router's server/client component split means your analytics module must be imported from a Client Component (mark the file or its importer with "use client"), and route-change tracking needs an explicit listener on the pathname since App Router doesn't fire a native page-load event on client-side navigation.

Still stuck, or want it checked properly?

The free scored audit reads your Amplitude instance across ten dimensions - taxonomy health, identity resolution, revenue tracking, and more - and hands back the three highest-payoff fixes. No commitment, 48-hour turnaround.

Request the free audit