---
published: false
description: "A practical guide to integrating MoEngage with React Native, including Android and iOS setup, event tracking, user identification, push notifications, and debugging events from the MoEngage dashboard."
tags: reactnative, javascript, mobile, analytics
---
# MoEngage + React Native: A Practical Guide to Events, User Tracking, and Push Notifications
When I first started integrating **MoEngage into a React Native application**, I expected the setup to be simple.
Install the SDK, initialise it, track some events, and move on.
The React Native part actually is simple.
The confusing part is everything around it.
There are several things that need to line up correctly:
- React Native SDK
- Android configuration
- iOS configuration
- Workspace ID
- Project ID
- Data centre
- Push notification configuration
- Event naming
- User identification
- Dashboard validation
Miss one of these and you can end up in a situation where the app builds successfully, the SDK appears to initialise correctly, but your events aren't where you expect them to be.
So in this post, I'll walk through the approach I used to integrate MoEngage into a React Native application and, more importantly, **how I verify that the integration is actually working**.
The goal is simple:
> Get MoEngage properly initialised, send a real event, find that event on the dashboard, and build from there.
---
## What Are We Trying to Achieve?
For my React Native application, MoEngage wasn't being used only for analytics.
The requirement included:
- Custom events
- User attributes
- User identification
- Push notifications
- In-app campaigns
- Campaign journeys
- Deep links
That's where MoEngage becomes more than an analytics SDK.
The idea is:
```text
User action
↓
React Native event
↓
MoEngage
↓
User profile / segmentation
↓
Campaign / journey
↓
Push / In-App / Email / SMS / WhatsApp
Once the application sends reliable events, the marketing team can use those events to create campaigns without requiring a new mobile app release for every campaign.
1. Install the React Native SDK
The first step is straightforward:
yarn add react-native-moengage
React Native autolinking handles the native linking, so there is no need to run:
react-native link
After installation, there is one Android configuration that is easy to miss.
Add the moengage configuration at the root level of package.json:
{
"name": "yourapp",
"moengage": {
"includeAndroidXRequiredLibraries": true,
"richNotification": true
}
}
What do these options do?
includeAndroidXRequiredLibraries enables the AndroidX libraries required by the SDK.
richNotification enables richer Android push notifications, such as:
- Images
- Big text
- Buttons
If you're planning to use rich push notifications, make sure this is configured.
2. My React Native Structure
I didn't want MoEngage calls scattered across different screens.
Instead, I kept the integration behind two files:
src/
└── moengage/
├── moengageEvents.js
└── MoEngageManager.js
The responsibilities are intentionally simple.
moengageEvents.js
Contains:
- Event names
- Event attributes
- User attribute keys
MoEngageManager.js
Contains:
- SDK initialisation
- Event tracking
- User identification
- User attributes
- Location
- Logout
- Push listeners
- In-app listeners
This gives the application a simple interface:
Screen
↓
MoEngageManager
↓
react-native-moengage
↓
Native SDK
3. Get Your MoEngage IDs
Before configuring Android or iOS, I recommend getting the identifiers from the dashboard.
There are two values you may need.
Workspace ID
MoEngage currently refers to this as the Workspace ID.
Older SDK parameters and documentation may refer to it as the App ID.
You can find it under:
Dashboard → Settings → General Settings → Workspace ID
Even though the dashboard calls it Workspace ID, the SDK still uses the parameter:
appId
So don't let the naming difference confuse you.
Project ID
If your MoEngage workspace uses Portfolio, you also need the Project ID.
You can find it under:
Dashboard → Settings → Portfolio → Project ID
This becomes particularly important when multiple projects exist inside the same workspace.
Without the correct Project ID, your events may not be associated with the project you expect.
If you're not using Portfolio, you can skip this configuration.
4. The Data Centre Is Important
This is one of the first things I check when events aren't showing up.
Your MoEngage dashboard URL tells you which data centre your workspace uses.
For example:
| Dashboard URL | Android | iOS |
|---|---|---|
dashboard-01.moengage.com |
DATA_CENTER_1 |
.data_center_01 |
dashboard-02.moengage.com |
DATA_CENTER_2 |
.data_center_02 |
dashboard-03.moengage.com |
DATA_CENTER_3 |
.data_center_03 |
So if you're using:
dashboard-02.moengage.com
your Android configuration should use:
DataCenter.DATA_CENTER_2
and iOS should use:
.data_center_02
My rule when debugging missing events
Before changing any JavaScript code, check:
Dashboard URL
↓
Data Centre
↓
Android / iOS SDK configuration
They should match.
5. Keep IDs in Environment Variables
I prefer not to hardcode MoEngage credentials directly into the native code.
For example:
# .env.production
MOENGAGE_APP_ID=YOUR_WORKSPACE_ID
MOENGAGE_PROJECT_ID=YOUR_PROJECT_ID
If you're using react-native-config, these values can then be exposed to both JavaScript and native code.
This also makes it easier to maintain separate configurations for:
- Development
- QA
- Staging
- Production
6. Android Setup
For Android, I initialise MoEngage from MainApplication.kt.
The important thing here is initialisation timing.
I initialise the SDK before React Native starts loading.
Why?
Because the application can be launched from a push notification while it is completely killed.
If the SDK isn't ready when the application launches, you can miss important notification or attribution information.
A simplified setup looks like this:
import com.moengage.core.DataCenter
import com.moengage.core.MoEngage
import com.moengage.core.config.NotificationConfig
import com.moengage.core.config.ProjectConfig
import com.moengage.react.MoEInitializer
class MainApplication : Application(), ReactApplication {
override fun onCreate() {
super.onCreate()
initializeMoEngage()
loadReactNative(this)
}
private fun initializeMoEngage() {
val moEngage = MoEngage.Builder(
this,
BuildConfig.MOENGAGE_APP_ID,
DataCenter.DATA_CENTER_2
)
.apply {
if (BuildConfig.MOENGAGE_PROJECT_ID.isNotEmpty()) {
configureProject(
ProjectConfig(
BuildConfig.MOENGAGE_PROJECT_ID
)
)
}
}
.configureNotificationMetaData(
NotificationConfig(
smallIcon = R.drawable.ic_notification,
largeIcon = R.mipmap.ic_launcher,
notificationColor = R.color.moe_notification_color,
isMultipleNotificationInDrawerEnabled = true
)
)
MoEInitializer.initializeDefaultInstance(
applicationContext,
moEngage
)
}
}
7. Android Notification Icon
One small Android detail caused me more confusion than I expected.
The notification icon is not the same as your normal application icon.
Android uses the alpha channel of the small notification icon.
So if you use a full-colour launcher icon, you may end up with something that looks like a solid white square in the status bar.
I recommend creating a dedicated:
ic_notification
icon with:
- White foreground
- Transparent background
- Proper Android density resources
For example:
mdpi
hdpi
xhdpi
xxhdpi
xxxhdpi
Then configure it in MoEngage:
smallIcon = R.drawable.ic_notification
8. Android Notification Colour
I also define a dedicated notification colour.
In:
android/app/src/main/res/values/colors.xml
<resources>
<color name="moe_notification_color">
#FF6B00
</color>
</resources>
Then:
notificationColor = R.color.moe_notification_color
9. Android Environment Configuration
If you're using react-native-config, your environment values need to reach Android's BuildConfig.
For example:
project.ext.envConfigFiles = [
debug : ".env",
release: ".env.production",
]
apply from: project(':react-native-config').projectDir.getPath()
+ "/dotenv.gradle"
Then the native code can access:
BuildConfig.MOENGAGE_APP_ID
BuildConfig.MOENGAGE_PROJECT_ID
If you're not using react-native-config, you can also define these directly:
android {
defaultConfig {
buildConfigField(
"String",
"MOENGAGE_APP_ID",
"\"YOUR_WORKSPACE_ID\""
)
buildConfigField(
"String",
"MOENGAGE_PROJECT_ID",
"\"YOUR_PROJECT_ID\""
)
}
}
10. Handle Configuration Changes
For in-app campaigns, I also forward configuration changes from MainActivity.kt.
import com.moengage.react.MoEReactHelper
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
MoEReactHelper
.getInstance()
.onConfigurationChanged()
}
This helps MoEngage respond correctly to configuration changes such as device rotation.
11. iOS Setup
The iOS setup follows the same basic concept but has its own native requirements.
First, add the MoEngage podspec source to your Podfile:
source 'https://github.com/CocoaPods/Specs.git'
source 'https://github.com/moengage/PodSpecs.git'
platform :ios, '15.1'
Then:
cd ios
pod install
cd ..
12. Configure iOS IDs
I use build settings rather than hardcoding the values.
In Info.plist:
<key>MoEngageAppId</key>
<string>$(MOENGAGE_APP_ID)</string>
<key>MoEngageProjectId</key>
<string>$(MOENGAGE_PROJECT_ID)</string>
This allows different build configurations to use different MoEngage environments.
13. Initialise MoEngage in AppDelegate
Just like Android, I initialise the SDK early.
For example:
import MoEngageCore
import MoEngagePluginBase
private func initializeMoEngage(
launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) {
let appId =
Bundle.main.object(
forInfoDictionaryKey: "MoEngageAppId"
) as? String ?? ""
let projectId =
Bundle.main.object(
forInfoDictionaryKey: "MoEngageProjectId"
) as? String ?? ""
let sdkConfig = MoEngageSDKConfig(
appId: appId,
dataCenter: .data_center_02
)
if !projectId.isEmpty {
sdkConfig.projectConfig =
MoEngageProjectConfig(
projectId: projectId
)
}
MoEngagePlugin().initializeDefaultInstance(
sdkConfig: sdkConfig,
launchOptions: launchOptions
)
}
The important part is to initialise MoEngage before the normal React Native application launch flow.
14. iOS Push Configuration
For iOS push notifications, make sure the application has:
- Push Notifications capability
- Background Modes
- Remote notifications
You also need to configure your APNs .p8 key in the MoEngage dashboard.
A common mistake here is assuming that receiving a device token means push is completely configured.
It doesn't.
You still need the APNs configuration on the MoEngage side for delivery.
15. Creating the MoEngage Manager
Now we get to the React Native side.
I keep all MoEngage calls inside:
src/moengage/MoEngageManager.js
The basic responsibilities are:
init()
track()
identify()
setUserAttributes()
setLocation()
logout()
I also keep push and in-app listeners inside this manager.
16. Initialise MoEngage
A simplified initialisation flow looks like this:
import ReactMoE, {
MoEInitConfig,
MoEPushConfig,
MoEngageLogConfig,
MoEngageLogLevel,
MoEAppStatus,
} from "react-native-moengage";
const init = ({ isFirstInstall = false } = {}) => {
const initConfig = new MoEInitConfig(
MoEPushConfig.defaultConfig(),
new MoEngageLogConfig(
MoEngageLogLevel.DEBUG,
true
)
);
ReactMoE.initialize(
appId,
initConfig
);
ReactMoE.setAppStatus(
isFirstInstall
? MoEAppStatus.Install
: MoEAppStatus.Update
);
};
For development, debug logs are useful.
For production, I reduce the log level.
17. Register Listeners Before Initialisation
This is one of the implementation details I consider important.
I register listeners before calling initialize().
For example:
ReactMoE.setEventListener(
"pushClicked",
payload => {
console.log("pushClicked", payload);
}
);
Why?
Because a push notification can launch the application from a killed state.
If the callback fires before the listener has been registered, you can lose the payload.
That can become particularly painful when the payload contains deep-link information.
My flow is therefore:
Register listeners
↓
Initialise MoEngage
↓
Start application
18. Track Custom Events
I use a single manager method for tracking:
track(eventName, props = {}) {
if (!isReady || !eventName) {
return;
}
try {
const properties = new MoEProperties();
Object.entries(props).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
properties.addAttribute(key, value);
}
});
ReactMoE.trackEvent(
eventName,
properties
);
} catch (error) {
console.log(
"[MoEngage] trackEvent failed",
error
);
}
}
I also keep the SDK call inside try/catch.
An analytics SDK should not be able to crash a user-facing screen because a tracking call failed.
If an event fails, I would rather lose that event than break the application.
19. Identify Users
After login, I identify the user:
MoEngageManager.identify(user.id, {
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
city: user.city,
});
I also identify the user again on application launch if the user is already logged in.
This is important because it allows the anonymous profile and known user profile to be associated correctly.
20. Use Dedicated User Attributes
For known MoEngage profile fields, I prefer using the dedicated SDK methods.
For example:
ReactMoE.setUserEmailID(
String(value)
);
ReactMoE.setUserContactNumber(
String(value)
);
ReactMoE.setUserFirstName(
String(value)
);
ReactMoE.setUserLastName(
String(value)
);
Instead of:
ReactMoE.setUserAttribute(
"email",
value
);
The dedicated methods give MoEngage the semantic information it expects for the user profile and engagement channels.
21. Keep Event Names in One Place
This is where moengageEvents.js comes in.
export const EVENTS = {
SIGNUP_COMPLETED: "signupCompleted",
PRODUCT_VIEWED: "productViewed",
ADD_TO_CART: "addToCart",
CHECKOUT_STARTED: "checkoutStarted",
PURCHASE_COMPLETED: "purchaseCompleted",
ITEM_SHARED: "itemShared",
};
export const KEYS = {
ITEM_ID: "itemId",
ITEM_NAME: "itemName",
CATEGORY: "category",
PRICE: "price",
CURRENCY: "currency",
QUANTITY: "quantity",
SHARE_CHANNEL: "shareChannel",
LOGIN_STATE: "loginState",
ACTION_DATE_TIME: "actionDateTime",
};
This might look like a small thing, but it becomes extremely valuable as the number of events grows.
22. Why Event Naming Matters
Event names become part of your analytics contract.
For example:
addToCart
and:
AddToCart
should not be treated as the same event.
The same applies to:
itemId
and:
item_id
If different developers use different naming conventions, you can end up with fragmented analytics.
I prefer:
Events
addToCart
checkoutStarted
purchaseCompleted
itemShared
Attributes
itemId
itemName
category
price
quantity
A simple rule that works well for me:
Use camelCase and keep the naming convention consistent from day one.
23. Send the Correct Data Types
Another thing I pay attention to is data type.
If the price is numeric:
price: 2499
don't send:
price: "2499"
Similarly:
isLoggedIn: true
is preferable to:
isLoggedIn: "true"
Correct data types become especially important when building segments and conditions later.
24. Calling Events From a Screen
Once the manager and constants are in place, screen-level code becomes simple.
For example:
const onAddToCart = (item, quantity) => {
MoEngageManager.track(
EVENTS.ADD_TO_CART,
{
[KEYS.ITEM_ID]: item.id,
[KEYS.ITEM_NAME]: item.name,
[KEYS.CATEGORY]: item.category,
[KEYS.PRICE]: item.price,
[KEYS.QUANTITY]: quantity,
[KEYS.LOGIN_STATE]: isLoggedIn
? "loggedIn"
: "guest",
[KEYS.ACTION_DATE_TIME]:
new Date().toISOString(),
}
);
};
Now the screen doesn't need to know anything about the native MoEngage SDK.
It only knows about:
MoEngageManager.track()
25. Logout
On logout:
MoEngageManager.logout();
This keeps the MoEngage profile lifecycle aligned with the application's authentication lifecycle.
26. The Most Important Part: How I Verify Events
Getting this far doesn't mean the integration is working.
The real test for me is:
Can I trigger an event and find it in the MoEngage dashboard?
I start with a test user.
Log in to the application using the test account and trigger something simple, such as:
addToCart
Then open the MoEngage dashboard.
Go to:
Dashboard → Segment → Search User
Search using one of the values associated with the user:
- User ID
- Mobile number
- Name
- MoEngage ID
27. Check User Attributes
Once I find the profile, I check the user attributes.
For example:
firstName
lastName
email
city
appVersion
This tells me whether the user identification flow is working correctly.
It's also a good place to catch naming issues.
If I see:
firstName
firstname
I know that two different naming conventions are being used somewhere in the application.
28. Check Events / Activity
Next, I look at the user's Events / Activity.
This is where I expect to see:
addToCart
I then expand the event and verify the properties:
itemId
itemName
category
price
quantity
I don't just check whether the event exists.
I also check whether the attributes and their values are correct.
An event can successfully arrive in MoEngage and still be incorrectly implemented.
29. Check Devices for Push Issues
If I'm debugging push notifications, I also check the user's device information.
I'm looking for things such as:
Platform
Device
SDK version
Push token
If there is no usable push token, I move away from the event-tracking code and start checking the native push configuration.
This separation makes debugging much faster.
30. What If the Event Doesn't Appear?
When an event doesn't show up, this is the order I normally follow.
1. Check the data centre
For example:
dashboard-02
should match:
DataCenter.DATA_CENTER_2
2. Wait for the SDK to flush
SDKs can batch events.
Don't assume an event is lost immediately.
3. Check user identification
If the user is anonymous, searching by your application's user ID won't work until the identity has been associated.
4. Check event spelling
Remember:
addToCart
and:
AddToCart
are different.
5. Enable debug logging
During development, I use debug/verbose logging to inspect the SDK behaviour.
For example:
new MoEngageLogConfig(
MoEngageLogLevel.DEBUG,
true
)
I make sure this is reduced for release builds.
My MoEngage Integration Checklist
Before I consider the integration complete, I go through this checklist:
- [ ] Workspace ID is correct
- [ ] Project ID is configured when Portfolio is enabled
- [ ] Data centre matches the dashboard
- [ ]
moengageblock exists inpackage.json - [ ] Rich notifications are enabled if required
- [ ] Android SDK initialises early
- [ ] Android notification icon is configured
- [ ] Android notification colour is configured
- [ ] Notification channels are configured
- [ ] Android push permission is handled
- [ ] Configuration changes are forwarded
- [ ] MoEngage CocoaPods source is configured
- [ ] iOS SDK initialises early
- [ ] Push Notifications capability is enabled
- [ ] Background remote notifications are enabled
- [ ] APNs credentials are configured
- [ ] Events are centralised
- [ ] Attribute names follow a consistent convention
- [ ] Correct data types are sent
- [ ] Listeners are registered before SDK initialisation
- [ ] Release logging is reduced
- [ ] At least one real event is visible on the dashboard
Final Thoughts
The actual React Native integration isn't particularly large.
The difficult part is making sure all the pieces around it agree with each other.
The approach that worked best for me was keeping the JavaScript integration intentionally small:
moengageEvents.js
↓
Event + attribute definitions
MoEngageManager.js
↓
SDK interaction
Then I treat the native Android and iOS configuration separately.
And when something doesn't work, I don't immediately start changing the event code.
I go through:
Workspace ID
↓
Project ID
↓
Data Centre
↓
Native SDK initialisation
↓
User identification
↓
Event tracking
↓
MoEngage dashboard
Once I can trigger one real event and find it on the dashboard, I know the foundation is working.
From there, adding more events becomes much easier.
If you're integrating MoEngage into a React Native app, my biggest recommendation would be:
Don't start by adding 50 events. Start with one event, get it visible in the dashboard, verify its attributes and types, and then scale the implementation.
That one test will tell you whether your integration is actually working.
Top comments (0)