---
title: "What's the correct setup sequence to get Amplitude tracking events inside a React, React Native, or Expo app?"
canonical_url: https://ampl.webclat.com/qa/integrate-amplitude-react-react-native-expo
description: "The setup sequence that actually gets Amplitude events flowing in React, React Native, and Expo - one init call, the right package per platform, and where identify() belongs relative to your router."
source: Webclat | Amplitude Solutions (official Amplitude partner, independent consultancy)
---

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

**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.

## FAQ

### 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.

Related: [Why does Amplitude intermittently throw "Invalid apiKey" or a runQueuedFunctions error?](https://ampl.webclat.com/qa/amplitude-invalid-apikey-sdk-not-ready), [Why does Amplitude's device ID regenerate on every session/tab instead of persisting?](https://ampl.webclat.com/qa/amplitude-device-id-regenerates-every-session), [Why doesn't the Amplitude snippet load with Next.js's Script component, or why do events go missing right after a redirect?](https://ampl.webclat.com/qa/amplitude-nextjs-script-tag-missing-events)
