Purely built for React Native, this library allows you to integrate PostHog with your React Native project. For React Native projects built with Expo, there are no mobile native dependencies outside of supported Expo packages.
Installation
In your React Native or Expo project add the posthog-react-native
package to your project as well as the required peer dependencies.
Expo apps
npx expo install posthog-react-native expo-file-system expo-application expo-device expo-localization
React Native apps
yarn add posthog-react-native @react-native-async-storage/async-storage react-native-device-info# ornpm i -s posthog-react-native @react-native-async-storage/async-storage react-native-device-info
React Native Web and macOS
If you're using React Native Web or React Native macOS, do not use the expo-file-system package since the Web and macOS targets aren't supported, use the @react-native-async-storage/async-storage package instead.
Configuration
With the PosthogProvider
The recommended way to set up PostHog for React Native is to use the PostHogProvider
which utilizes the Context API to pass the PostHog client around, enable autocapture, and ensure that the queue is flushed at the right time:
// App.(js|ts)import { usePostHog, PostHogProvider } from 'posthog-react-native'...export function MyApp() {return (<PostHogProvider apiKey="<ph_project_api_key>" options={{// usually 'https://app.posthog.com' or 'https://eu.posthog.com'host: '<ph_instance_address>',}}><MyComponent /></PostHogProvider>)}// Now you can simply access PostHog elsewhere in the app like so:const MyComponent = () => {const posthog = usePostHog()useEffect(() => {posthog?.capture("mycomponent_loaded", { foo: "bar" })}, [])}
Configuration
Without the PosthogProvider
Due to the async nature of React Native, PostHog needs to be initialized asynchronously for the persisted state to load properly. The PosthogProvider
takes care of this under-the-hood but you can alternatively create the instance yourself like so:
// posthog.tsimport PostHog from 'posthog-react-native'export let posthog: PostHog | undefined = undefinedexport const posthogAsync: Promise<PostHog> = PostHog.initAsync('<ph_project_api_key>', {// usually 'https://app.posthog.com' or 'https://eu.posthog.com'host: '<ph_instance_address>'})posthogAsync.then(client => {posthog = client})// app.tsimport { posthog, posthogAsync} from './posthog'export function MyApp1() {useEffect(async () => {// Use posthog optionally with the possibility that it may still be loadingposthog?.capture('myapp1_loaded')// OR use posthog via the promise(await posthogAsync).capture('myapp1_loaded')}, [])return <View>Your app code</View>}// You can even use this instance with the PostHogProviderexport function MyApp2() {return <PostHogProvider client={posthogAsync}>{/* Your app code */}</PostHogProvider>}
Configuration options
You can further customize how PostHog works through its configuration on initialization.
export const posthog = await PostHog.initAsync("<ph_project_api_key>", {// PostHog API host, usually 'https://app.posthog.com' or 'https://eu.posthog.com'host?: string = "<ph_instance_address>",// The number of events to queue before sending to PostHog (flushing)flushAt?: number = 20,// The interval in milliseconds between periodic flushesflushInterval?: number = 10000// If set to false, tracking will be disabled until 'optIn' is calledenable?: boolean = true,// Whether to track that 'getFeatureFlag' was called (used by Experiments)sendFeatureFlagEvent?: boolean = true,// Whether to load feature flags when initialised or notpreloadFeatureFlags?: boolean = true,// How many times we will retry HTTP requestsfetchRetryCount?: number = 3,// The delay between HTTP request retriesfetchRetryDelay?: number = 3000,// For Session Analysis how long before we expire a sessionsessionExpirationTimeSeconds?: number = 1800 // 30 mins// Whether to post events to PostHog in JSON or compressed formatcaptureMode?: 'json' | 'form'})
Usage
Capturing events
You can send custom events using capture
:
posthog.capture('user_signed_up')
Tip: We recommend using a '[object][verb]' format for your event names, where '[object]' is the entity that the behavior relates to, and '[verb]' is the behavior itself. For example, project created
, user signed up
, or invite sent
.
Setting event properties
Optionally, you can also include additional information in the event by setting the properties value:
posthog.capture('user_signed_up', {login_type: "email",is_free_trial: true})
Autocapture
PostHog autocapture automatically tracks the following events for you:
- Application Opened - when the app is opened from a closed state
- Application Became Active - when the app comes to the foreground (e.g. from the app switcher)
- Application Backgrounded - when the app is sent to the background by the user
- Application Installed - when the app is installed.
- Application Updated - when the app is updated.
- $screen - when the user navigates (if using
@react-navigation/native
orreact-native-navigation
) - $autocapture - touch events when the user interacts with the screen
With autocapture, all touch events for children of PosthogProvider
are tracked, capturing a snapshot of the view hierarchy at that point. This enables you to create insights in PostHog without having to add custom events.
PostHog will try to generate a sensible name for the touched element based on the React component displayName
or name
but you can force this to something reliable (and also clearly marked for PostHog tracking) using the ph-label
prop.
<View ph-label="my-special-label"></View>
If there are elements you don't want to be captured, you can add the ph-no-capture
property like so. If this property is found anywhere in the view hierarchy, the entire touch event is ignored.
<View ph-no-capture>Sensitive view here</View>
Tracking Screen views and touches with @react-navigation/native
:
// App.(js|ts)import { PostHogProvider } from 'posthog-react-native'import { NavigationContainer } from '@react-navigation/native'export function App() {return (<NavigationContainer><PostHogProvider apiKey="<ph_project_api_key>" autocapture>{/* Rest of app */}</PostHogProvider></NavigationContainer>)}
Tracking Screen views and touches with react-native-navigation
:
import PostHog, { PostHogProvider } from 'posthog-react-native'import { Navigation } from 'react-native-navigation';export const posthogAsync = PostHog.initAsync('<ph_project_api_key>');// Simplify the wrapping of your Screens with a shared PostHogProviderexport const SharedPostHogProvider = (props: any) => {return (<PostHogProvider client={posthogAsync} autocapture={{captureLifecycleEvents: false, // Lifecycle events are handled differently for react-native-navigationcaptureTouches: true,}}>{props.children}</PostHogProvider>);};export const MyScreen = () => {return (// Every screen needs to be wrapped with this provider if you want to capture touches or use the hook `usePostHog()`<SharedPostHogProvider><View>...</View></SharedPostHogProvider>);};Navigation.registerComponent('Screen', () => MyScreen);Navigation.events().registerAppLaunchedListener(async () => {(await posthogAsync).initReactNativeNavigation({navigation: {// (Optional) Set the name based on the route. Defaults to the route name.routeToName: (name, properties) => name,// (Optional) Tracks all passProps as properties. Defaults to undefinedrouteToProperties: (name, properties) => properties,},});});
Autocapture configuration
You can tweak how autocapture works by passing custom options.
<PostHogProvider apiKey="<ph_project_api_key>" autocapture={{captureTouches: true, // If you don't want to capture touch events set this to falsecaptureLifecycleEvents: true, // If you don't want to capture the Lifecycle Events (e.g. Application Opened) set this to falsecaptureScreens: true, // If you don't want to capture screen events set this to falseignoreLabels: [], // Any labels here will be ignored from the stack in touch eventscustomLabelProp: "ph-label",noCaptureProp: "ph-no-capture",propsToCapture = ["testID"], // Limit which props are captured. By default, identifiers and text content are captured.navigation: {// By default only the Screen name is tracked but it is possible to track the// params or modify the name by intercepting theautocapture like sorouteToName: (name, params) => {if (params.id) return `${name}/${params.id}`return name},routeToParams: (name, params) => {if (name === "SensitiveScreen") return undefinedreturn params},}}}>...</PostHogProvider>
Capturing screen views
With the PostHogProvider
, screen tracking is automatically captured if the autocapture
property is used. Currently only @react-navigation/native is supported by autocapture and it is important that the PostHogProvider
is configured as a child of the NavigationContainer
.
If you want to manually send a new screen capture event, use the screen
function. This function requires a name
. You may also pass in an optional properties
object.
posthog.screen('dashboard', {background: 'blue',hero: 'superhog',})
Identify
We highly recommend reading our section on Identifying users to better understand how to correctly use this method.
When you start tracking events with PostHog, each user gets an anonymous ID that is used to identify them in the system.
In order to link this anonymous user with someone from your database, use the identify
call.
Identify lets you add metadata on your users so you can more easily identify who they are in PostHog, and even do things like segment users by these properties.
An identify call requires:
distinctId
which uniquely identifies your user in your databaseuserProperties
with a dictionary with key: value pairs
posthog.identify('distinctID', {email: 'user@posthog.com',name: 'My Name'})
The most obvious place to make this call is whenever a user signs up, or when they update their information.
When you call identify
, all previously tracked anonymous events will be linked to the user.
Setting user properties via an event
When capturing an event, you can pass a property called $set
as an event property, and specify its value to be an object with properties to be set on the user that will be associated with the user who triggered the event.
$set
Example
posthog.capture('some_event', { $set: { userProperty: 'value' } })
Usage
When capturing an event, you can pass a property called $set
as an event property, and specify its value to be an object with properties to be set on the user that will be associated with the user who triggered the event.
$set_once
Example
posthog.capture('some_event', { $set_once: { userProperty: 'value' } })
Usage
$set_once
works just like $set
, except that it will only set the property if the user doesn't already have that property set.
Super Properties
Super Properties are properties associated with events that are set once and then sent with every capture
call, be it a $screen, an autocaptured touch, or anything else.
They are set using posthog.register
, which takes a properties object as a parameter, and they persist across sessions.
For example, take a look at the following call:
posthog.register({'icecream pref': 'vanilla',team_id: 22,})
The call above ensures that every event sent by the user will include "icecream pref": "vanilla"
and "team_id": 22
. This way, if you filtered events by property using icecream_pref = vanilla
, it would display all events captured on that user after the posthog.register
call, since they all include the specified Super Property.
However, please note that this does not store properties against the User, only against their events. To store properties against the User object, you should use posthog.identify
. More information on this can be found on the Sending User Information section.
Removing stored Super Properties
Super Properties are persisted across sessions so you have to explicitly remove them if they are no longer relevant. In order to stop sending a Super Property with events, you can use posthog.unregister
, like so:
posthog.unregister('icecream pref'),
This will remove the Super Property and subsequent events will not include it.
If you are doing this as part of a user logging out you can instead simply use posthog.reset
which takes care of clearing all stored Super Properties and more.
Flush
You can set the number of events in the configuration that should queue before flushing.
Setting this to 1
will send events immediately and will use more battery. This is set to 20
by default.
You can also configure the flush interval. By default we flush all events after 30
seconds,
no matter how many events have gathered.
You can also manually flush the queue, like so:
posthog.flush()// or using async/awaitawait posthog.flushAsync()
Reset
To reset the user's ID and anonymous ID, call reset
. Usually you would do this right after the user logs out.
posthog.reset()
Opt in/out
By default, PostHog has tracking enabled unless it is forcefully disabled by default using the option { enable: false }
.
You can give your users the option to opt in or out by calling the relevant methods. Once these have been called they are persisted and will be respected until optIn/Out is called again or the reset
function is called.
To Opt in/out of tracking, use the following calls.
posthog.optIn() // opt inposthog.optOut() // opt out
If you still wish capture these events but want to create a distinction between users and team in PostHog, you should look into Cohorts.
Feature Flags
PostHog's feature flags enable you to safely deploy and roll back new features.
There are two ways to implement feature flags in React Native:
- Using hooks.
- Loading the flag directly.
Method 1: Using hooks
Example 1: Boolean feature flags
import { useFeatureFlag } from 'posthog-react-native'const MyComponent = () => {const booleanFlag = useFeatureFlag('key-for-your-boolean-flag')if (showFlaggedFeature === undefined) {// the response is undefined if the flags are being loadedreturn null}return showFlaggedFeature ? <Text>Testing feature 😄</Text> : <Text>Not Testing feature 😢</Text>}
Example 2: Multivariate feature flags
import { useFeatureFlag } from 'posthog-react-native'const MyComponent = () => {const multiVariantFeature = useFeatureFlag('key-for-your-multivariate-flag')if (multiVariantFeature === undefined) {// the response is undefined if the flags are being loadedreturn null} else if (multiVariantFeature === 'variant-name') { // replace 'variant-name' with the name of your variant// Do something}return <div/>}
Method 2: Loading the flag directly
// Defaults to undefined if not loaded yet or if there was a problem loadingposthog.getFeatureFlag('key-for-your-boolean-flag')// Multivariant feature flags are returned as a stringposthog.getFeatureFlag('key-for-your-multivariate-flag')
Reloading flags
PostHog loads feature flags when instantiated and refreshes whenever methods are called that affect the flag.
If you have the need to forcefully trigger the refresh however you can use reloadFeatureFlagsAsync()
:
posthog.reloadFeatureFlagsAsync().then((refreshedFlags) => console.log(refreshedFlags))
Bootstrapping Flags
Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This is detrimental if you want to do something like redirecting to a different page based on a feature flag.
To have your feature flags available immediately, you can initialize PostHog with precomputed values until PostHog has had a chance to fetch them. This is called bootstrapping.
For details on how to implement bootstrapping, see our bootstrapping guide.
Experiments (A/B tests)
Since experiments use feature flags, the code for running an experiment is very similar to the feature flags code:
// With the useFeatureFlag hookimport { useFeatureFlag } from 'posthog-react-native'const MyComponent = () => {const variant = useFeatureFlag('experiment-feature-flag-key')if (variant === undefined) {// the response is undefined if the flags are being loadedreturn null}if (variant == 'variant-name') {// do something}}
It's also possible to run experiments without using feature flags.
Group analytics
Group analytics allows you to associate the events for that person's session with a group (e.g. teams, organizations, etc.). Read the Group Analytics guide for more information.
Note: This is a paid feature and is not available on the open-source or free cloud plan. Learn more here.
- Associate the events for this session with a group
posthog.group('company', 'company_id_in_your_db')posthog.capture('upgraded_plan') // this event is associated with company ID `company_id_in_your_db`
- Associate the events for this session with a group AND update the properties of that group
posthog.group('company', 'company_id_in_your_db', {name: 'Awesome Inc.',employees: 11,})
The name
is a special property which is used in the PostHog UI for the name of the Group. If you don't specify a name
property, the group ID will be used instead.
Upgrading from V1 to V2
V1 of this library utilised the underlying posthog-ios
and posthog-android
SDKs to do most of the work. Since the new version is written entirely in JS, using only Expo supported libraries, there are some changes to the way PostHog is configured as well as actually calling PostHog.
For iOS, the new React Native SDK will attempt to migrate the previously persisted data (such as distinctId
and anonymousId
) which should result in no unexpected changes to tracked data.
For Android, it is unfortunately not possible for persisted Android data to be loaded which means stored information such as the randomly generated anonymousId
or the distinctId
set by posthog.identify
will not be present. For identified users, the simple workaround is to ensure that identify
is called at least once when the app loads. For anonymous users there is unfortunately no straightforward workaround they will show up as new anonymous users in PostHog.
Events such as Application Installed
and Application Updated
that require previously persisted data were unable to be migrated, the side effect being that you may see much higher numbers for Application Installed
events. This is due to the fact that there is no native way of detecting a real "install" and as such, we store a marker the first time the SDK loads and treat that as an install.
// DEPRECATED V1 Setupimport PostHog from 'posthog-react-native'await PostHog.setup('phc_ChmcdLbC770zgl23gp3Lax6SERzC2szOUxtp0Uy4nTf', {// usually 'https://app.posthog.com' or 'https://eu.posthog.com'host: '<ph_instance_address>',captureApplicationLifecycleEvents: false, // Replaced by 'PostHogProvider'captureDeepLinks: false, // No longer supportedrecordScreenViews: false, // Replaced by 'PostHogProvider' supporting @react-navigation/nativeflushInterval: 30, // Stays the sameflushAt: 20, // Stays the sameandroid: {...}, // No longer needediOS: {...}, // No longer needed})PostHog.capture("foo")// V2 Setup differenceimport PostHog from 'posthog-react-native'const posthog = await Posthog.initAsync('phc_ChmcdLbC770zgl23gp3Lax6SERzC2szOUxtp0Uy4nTf', {// usually 'https://app.posthog.com' or 'https://eu.posthog.com'host: '<ph_instance_address>',// Add any other options here.})// Use created instance rather than the PostHog classposthog.capture("foo")