Installation
Our React Native enables 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.
To install, 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
. This utilizes the Context API to pass the PostHog client around, enable autocapture, and ensure that the queue is flushed at the right time.
To set up PostHogProvider
, add it to your App.js
or App.ts
file:
// App.(js|ts)import { usePostHog, PostHogProvider } from 'posthog-react-native'...export function MyApp() {return (<PostHogProvider apiKey="<ph_project_api_key>" options={{// usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'host: 'https://us.i.posthog.com',}}><MyComponent /></PostHogProvider>)}
Then you can access PostHog using the usePostHog()
hook:
const MyComponent = () => {const posthog = usePostHog()useEffect(() => {posthog.capture("event_name")}, [posthog])}
Without the PosthogProvider
If you prefer not to use the provider, you can initialize PostHog in its own file and import the instance from there:
import PostHog from 'posthog-react-native'export const posthog = new PostHog('<ph_project_api_key>', {// usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'host: 'https://us.i.posthog.com'})
Then you can access PostHog by importing your instance:
import { posthog } from './posthog'export function MyApp1() {useEffect(async () => {posthog.capture('event_name')}, [posthog])return <View>Your app code</View>}
You can even use this instance with the PostHogProvider:
import { posthog } from './posthog'export function MyApp() {return <PostHogProvider client={posthog}>{/* Your app code */}</PostHogProvider>}
Configuration options
You can further customize how PostHog works through its configuration on initialization.
Attribute | Description |
---|---|
host Type: String Default: https://us.i.posthog.com | PostHog API host (usually https://us.i.posthog.com by default or https://eu.i.posthog.com ). Host is optional if you use https://us.i.posthog.com . |
flushAt Type: Number Default: 20 | The number of events to queue before sending to PostHog (flushing). |
flushInterval Type: Number Default: 10000 | The interval in milliseconds between periodic flushes. |
maxBatchSize Type: Number Default: 100 | The maximum number of queued messages to be flushed as part of a single batch (must be higher than flushAt ). |
maxQueueSize Type: Number Default: 1000 | The maximum number of cached messages either in memory or on the local storage (must be higher than flushAt ). |
disabled Type: Boolean Default: false | If set to true, the SDK is essentially disabled (useful for local environments where you don't want to track anything). |
defaultOptIn Type: Boolean Default: true | If set to false, the SDK will not track until the optIn() function is called. |
sendFeatureFlagEvent Type: Boolean Default: true | Whether to track that getFeatureFlag was called (used by experiments). |
preloadFeatureFlags Type: Boolean Default: true | Whether to load feature flags when initialized or not. |
bootstrap Type: Object Default: {} | An object containing the distinctId , isIdentifiedId , featureFlags , and featureFlagPayloads keys. distinctId is a string, and featureFlags and featureFlagPayloads are objects of key-value pairs. Used to ensure data is available as soon as the SDK loads. |
fetchRetryCount Type: Number Default: 3 | How many times HTTP requests will be retried. |
fetchRetryDelay Type: Number Default: 3000 | The delay between HTTP request retries. |
requestTimeout Type: Number Default: 10000 | Timeout in milliseconds for any calls. |
featureFlagsRequestTimeoutMs Type: Number Default: 10000 | Timeout in milliseconds for feature flag calls. |
sessionExpirationTimeSeconds Type: Number Default: 1800 | For session analysis, how long before a session expires (defaults to 30 minutes). |
captureMode Type: String Default: form | Whether to post events to PostHog in JSON or compressed format. |
persistence Type: String Default: file | Allows you to provide the storage type. file will try to load the best available storage, the provided customStorage , customAsyncStorage , or in-memory storage. |
customAppProperties Type: Object or Function Default: null | Allows you to provide your own implementation of the common information about your App or a function to modify the default App properties generated. |
customStorage Type: Object Default: null | Allows you to provide a custom asynchronous storage such as async-storage , expo-file-system , or a synchronous storage such as mmkv . If not provided, PostHog will attempt to use the best available storage via optional peer dependencies. If persistence is set to memory , this option is ignored. |
captureNativeAppLifecycleEvents Type: Boolean Default: false | Captures native app lifecycle events such as Application Installed, Application Updated, Application Opened, Application Became Active, and Application Backgrounded. By default, this is false. If you're already using the captureLifecycleEvents options with initReactNativeNavigation or PostHogProvider you should keep this as false, otherwise you may see duplicated events. |
disableGeoip Type: Boolean Default: false | When true, disables automatic GeoIP resolution for events and feature flags. |
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
, orinvite 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})
Capturing screen views
With @react-navigation/native
and autocapture:
When using @react-navigation/native, screen tracking is automatically captured if the autocapture
property is used in the PostHogProvider
:
It is important that the PostHogProvider
is configured as a child of the NavigationContainer
:
// 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>)}
With react-native-navigation
and autocapture:
First, simplify the wrapping of your screens with a shared PostHogProvider:
import PostHog, { PostHogProvider } from 'posthog-react-native'import { Navigation } from 'react-native-navigation';export const posthog = new PostHog('<ph_project_api_key>');export const SharedPostHogProvider = (props: any) => {return (<PostHogProvider client={posthog} autocapture={{captureScreens: false, // Screen events are handled differently for react-native-navigationcaptureLifecycleEvents: false, // Lifecycle events are handled differently for react-native-navigationcaptureTouches: true,}}>{props.children}</PostHogProvider>);};
Then, every screen needs to be wrapped with this provider if you want to capture touches or use the usePostHog()
hook
export const MyScreen = () => {return (<SharedPostHogProvider><View>...</View></SharedPostHogProvider>);};Navigation.registerComponent('Screen', () => MyScreen);Navigation.events().registerAppLaunchedListener(async () => {posthog.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,},captureScreens: true,captureLifecycleEvents: true,});});
Manually capturing screen capture events
If you prefer not to use autocapture, you can manually capture screen views by calling posthog.screen()
. This function requires a name
. You may also pass in an optional properties
object.
posthog.screen('dashboard', {background: 'blue',hero: 'superhog',})
Autocapture
PostHog autocapture can automatically track 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
. If you prefer, you can set your own name using the ph-label
prop:
<View ph-label="my-special-label"></View>
Autocapture configuration
You can tweak how autocapture works by passing custom options.
<PostHogProvider apiKey="<ph_project_api_key>" autocapture={{captureTouches: true,captureLifecycleEvents: true,captureScreens: true,ignoreLabels: [], // 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 the autocapture like sorouteToName: (name, params) => {if (params.id) return `${name}/${params.id}`return name},routeToProperties: (name, params) => {if (name === "SensitiveScreen") return undefinedreturn params},}}}>...</PostHogProvider>
Preventing sensitive data capture
If there are elements you don't want to be captured, you can add the ph-no-capture
property. If this property is found anywhere in the view hierarchy, the entire touch event is ignored:
<View ph-no-capture>Sensitive view here</View>
Identifying users
We highly recommend reading our section on Identifying users to better understand how to correctly use this method.
Using identify
, you can associate events with specific users. This enables you to gain full insights as to how they're using your product across different sessions, devices, and platforms.
An identify
call has the following arguments:
- distinctId: Required. A unique identifier for your user. Typically either their email or database ID.
- properties: Optional. A dictionary with key:value pairs to set the person properties
posthog.identify('distinctID',{ // ($set):name: 'My Name'})
$set_once
works just like $set
, except that it will only set the property if the user doesn't already have that property set. See the difference between $set
and $set_once
posthog.identify('distinctID',{$set: {name: 'My Name'},$set_once: {date_of_first_log_in: '2024-03-01'}})
You should call identify
as soon as you're able to. Typically, this is every time your app loads for the first time as well as directly after your user logs in. This ensures that events sent during your user's sessions are correctly associated with them.
When you call identify
, all previously tracked anonymous events will be linked to the user.
Get the current user's distinct ID
You may find it helpful to get the current user's distinct ID. For example, to check whether you've already called identify
for a user or not.
To do this, call posthog.get_distinct_id()
. This returns either the ID automatically generated by PostHog or the ID that has been passed by a call to identify()
.
Alias
Sometimes, you want to assign multiple distinct IDs to a single user. This is helpful when your primary distinct ID is inaccessible. For example, if a distinct ID used on the frontend is not available in your backend.
In this case, you can use alias
to assign another distinct ID to the same user.
// Sets alias for current userposthog.alias('distinct_id')
We strongly recommend reading our docs on alias to best understand how to correctly use this method.
Setting person properties
Person properties enable you to capture, manage, and analyze specific data about a user. You can use them to create filters or cohorts, which can then be used in insights, feature flags, and more.
To set a user's properties, include the $set
or $set_once
property when capturing any event:
$set
posthog.capture('some_event', { $set: { userProperty: 'value' } })
$set_once
$set_once
works just like $set
, except it only sets the property if the user doesn't already have that property set.
posthog.capture('some_event', { $set_once: { userProperty: 'value' } })
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:
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.
This does not set the user's properties. This only sets the properties for their events. To store person properties, see the setting person properties 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 posthog.reset()
which takes care of clearing all stored Super Properties and more.
Opt out of data capture
You can completely opt-out users from data capture. To do this, there are two options:
- Opt users out by default by setting
opt_out_capturing_by_default
totrue
in your PostHog config:
posthog.init('<ph_project_api_key>', {opt_out_capturing_by_default: true,});
- Opt users out on a per-person basis by calling
opt_out_capturing()
:
posthog.opt_out_capturing()
Similarly, you can opt users in:
posthog.opt_in_capturing()
To check if a user is opted out:
posthog.has_opted_out_capturing()
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. If a flush is already in progress it returns a promise for the existing flush.
await posthog.flush()
Reset after logout
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 { defaultOptIn: 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.optedOut // See if a user has opted outposthog.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}// Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payloadreturn 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}// Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payloadreturn <div/>}
Method 2: Loading the flag directly
// Defaults to undefined if not loaded yet or if there was a problem loadingposthog.isFeatureEnabled('key-for-your-boolean-flag')// 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')// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loadingposthog.getFeatureFlagPayload('key-for-your-multivariate-flag')
Reloading flags
PostHog loads feature flags when instantiated and refreshes whenever methods are called that affect the flag.
If want to manually trigger a refresh, you can call reloadFeatureFlagsAsync()
:
posthog.reloadFeatureFlagsAsync().then((refreshedFlags) => console.log(refreshedFlags))
Or when you want to trigger the reload, but don't care about the result:
posthog.reloadFeatureFlags()
Request timeout
You can configure the featureFlagsRequestTimeoutMs
parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 10 seconds.
export const posthog = new PostHog('<ph_project_api_key>', {// usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'host: 'https://us.i.posthog.com',featureFlagsRequestTimeoutMs: 10000 // Time in milliseconds. Default is 10000 (10 seconds).})
Error handling
When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler:
function handleFeatureFlag(client, flagKey, distinctId) {try {const isEnabled = client.isFeatureEnabled(flagKey, distinctId);console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`);return isEnabled;} catch (error) {console.error(`Error fetching feature flag '${flagKey}': ${error.message}`);// Optionally, you can return a default value or throw the error// return false; // Default to disabledthrow error;}}// Usage exampletry {const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123');if (flagEnabled) {// Implement new feature logic} else {// Implement old feature logic}} catch (error) {// Handle the error at a higher levelconsole.error('Feature flag check failed, using default behavior');// Implement fallback logic}
Overriding server properties
Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls:
posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'})
Note that these are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation.
Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading:
posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false)
At any point, you can reset these properties by calling resetPersonPropertiesForFlags
:
posthog.resetPersonPropertiesForFlags()
The same holds for group properties:
// set properties for a groupposthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}})// reset properties for all groups:posthog.resetGroupPropertiesForFlags()
Note: You don't need to add the group names here, since these properties are automatically attached to the current group (set via
posthog.group()
). When you change the group, these properties are reset.
Automatic overrides
Whenever you call posthog.identify
with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call posthog.group()
.
Default overridden properties
By default, we always override some properties based on the user IP address.
The list of properties that this overrides:
- $geoip_city_name
- $geoip_country_name
- $geoip_country_code
- $geoip_continent_name
- $geoip_continent_code
- $geoip_postal_code
- $geoip_time_zone
This enables any geolocation-based flags to work without manually setting these properties.
Bootstrapping Flags
Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag.
To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. After the SDK fetches feature flags from PostHog, it will use those flag values instead of bootstrapped ones.
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.
Session replay
To set up session replay in your project, all you need to do is install the React Native SDK and the Session replay plugin, enable "Record user sessions" in your project settings and enable the enableSessionReplay
option.
Disabling for local development
You may want to disable PostHog when working locally or in a test environment. You can do this by setting the disable
option to true
when initializing PostHog. Helpfully this allows you to continue using usePostHog
and safely calling it without anything actually happening.
// App.(js|ts)import { usePostHog, PostHogProvider } from 'posthog-react-native'...export function MyApp() {return (<PostHogProvider apiKey="<ph_project_api_key>" options={{// Disable PostHog in development (or whatever other logic you choose)disabled: __DEV__,}}><MyComponent /></PostHogProvider>)}const MyComponent = () => {const posthog = usePostHog()useEffect(() => {// Safe to call even when disabled!posthog.capture("mycomponent_loaded", { foo: "bar" })}, [])}
Upgrading from V1 or V2 to V3
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('<ph_project_api_key>', {// usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'host: 'https://us.i.posthog.com',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('<ph_project_api_key>', {// usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'host: '<ph_client_api_host>',// Add any other options here.})// Use created instance rather than the PostHog classposthog.capture("foo")// V3 Setup differenceimport PostHog from 'posthog-react-native'const posthog = new PostHog('<ph_project_api_key>', {// usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'host: '<ph_client_api_host>',// Add any other options here.})// Use created instance rather than the PostHog classposthog.capture("foo")