Skip to content

React Native

The SDK ships a dedicated React Native entry point@magicfeedback/popup-sdk/react-native — that does the whole integration for you: persistent storage, device info, platform, app lifecycle, crash capture, and rendering surveys in a Modal + WebView.

For most apps the integration is one component: wrap your app in <DeepdotsProvider>.

Terminal window
npm install @magicfeedback/popup-sdk react-native-webview

Two optional packages unlock the rest of the feature set. Install both unless you have a reason not to:

Terminal window
npm install react-native-mmkv react-native-device-info
Terminal window
cd ios && pod install
PackageRequiredWhat it gives you
react-native-webviewRenders the survey UI
react-native-mmkvRecommendedPersists the user_id across app restarts (returning users)
react-native-device-infoRecommendedDevice type, OS version, model, app version (Technology metrics)

All of them are declared as optional peer dependencies: the SDK detects what is installed at runtime and degrades gracefully. Without MMKV the user_id lives in memory only and a fresh one is generated on every launch; without react-native-device-info the device context is omitted.

App.tsx
import { DeepdotsProvider } from '@magicfeedback/popup-sdk/react-native';
export default function App() {
return (
<DeepdotsProvider
config={{
apiKey: 'YOUR_PUBLIC_API_KEY',
nodeEnv: __DEV__ ? 'development' : 'production',
userId: 'customer-123',
appVersion: '1.4.0',
// Omit `analytics` to stay in dry-run mode (events logged, nothing sent).
analytics: {
publicKey: 'YOUR_ANALYTICS_PUBLIC_KEY',
integration: 'YOUR_INTEGRATION_ID',
},
}}
>
<YourNavigation />
</DeepdotsProvider>
);
}

config accepts the same object as init(config) — the Provider fills in the React Native specific fields (storage, device, platform) for you.

ConcernHow
Persistent identityMMKV instance (id: 'deepdots-sdk') when react-native-mmkv is installed
Device inforeact-native-device-info when installed
PlatformPlatform.OS'ios' / 'android' in the analytics context
Engagement timeAppStateonForeground() / onBackground() (flushes on background)
Crash captureglobal.ErrorUtils → unhandled JS errors as deepdots_app_crash
Survey renderingReactNativePopupRenderer + a Modal with a WebView, mounted on demand

Two things are not automatic and need a few lines from you: navigation tracking and starting the triggers.

useDeepdots() returns the shared DeepdotsPopups instance from anywhere under the Provider. The full analytics API is available — see the Analytics guide.

import { useDeepdots } from '@magicfeedback/popup-sdk/react-native';
function ProductScreen() {
const dd = useDeepdots();
return (
<Button
title="Add to cart"
onPress={() => {
dd.track('add_to_cart', { product_id: 'p-123', value: 49.9 });
dd.triggerEvent('added_to_cart');
}}
/>
);
}

The Provider initializes the SDK and fetches the popup definitions, but it does not start the triggers — call autoLaunch() once from a component inside the Provider:

function DeepdotsBootstrap() {
const dd = useDeepdots();
useEffect(() => {
dd.autoLaunch();
}, [dd]);
return null;
}

Alternatively, skip autoLaunch() and show popups imperatively with dd.triggerEvent('some_event') — matching event triggers fire immediately.

A popup restricted to certain languages (segments.lang) is evaluated against the language the SDK resolved at init(): your explicit language, else navigator.language, else the Intl locale. That last fallback is what makes it work under Hermes, where navigator.language does not exist. Matching is by prefix, so a segment of en matches en-US.

If you want to control targeting explicitly instead of relying on detection — recommended when your app has its own i18n — pass the language in the config:

<DeepdotsProvider config={{ apiKey: 'YOUR_PUBLIC_API_KEY', language: 'es-ES' }}>

When a popup is shown, the Provider mounts a full-screen Modal containing a WebView with the survey. The survey HTML is self-contained but loads @magicfeedback/native from a CDN, so rendering a survey requires network access. Popup status (SHOWED / PARTIAL / COMPLETED) is reported to POST /sdk/popups automatically, and the persistent user_id, session_id, and active mini_service are injected into the survey’s identity metadata.

React Native has no History API, so screen views are not detected automatically. Report them with setScreen(name) — the SDK emits a deepdots_page_view event with the duration of the previous screen when you report the next one.

With React Navigation:

import { NavigationContainer, createNavigationContainerRef } from '@react-navigation/native';
import { useDeepdots } from '@magicfeedback/popup-sdk/react-native';
const navRef = createNavigationContainerRef();
function Navigation() {
const dd = useDeepdots();
const report = () => {
const route = navRef.getCurrentRoute();
if (route) dd.setScreen(route.name);
};
return (
<NavigationContainer ref={navRef} onReady={report} onStateChange={report}>
<YourStack />
</NavigationContainer>
);
}

Screen names are normalized the same way as web paths: query strings are dropped and numeric/UUID segments collapse to :id, so /product/123 and /product/456 both report as /product/:id.

Advanced: manual setup without the Provider

Section titled “Advanced: manual setup without the Provider”

If you need your own surface (a bottom sheet, a dedicated screen) or your own React tree, use setupReactNative() and the ReactNativePopupRenderer directly. Both come from the main entry point.

import { useEffect, useRef, useState } from 'react';
import { AppState, Modal, Platform, View } from 'react-native';
import { WebView } from 'react-native-webview';
import { MMKV } from 'react-native-mmkv';
import DeviceInfo from 'react-native-device-info';
import {
DeepdotsPopups,
ReactNativePopupRenderer,
setupReactNative,
type ReactNativeSurveyPayload,
} from '@magicfeedback/popup-sdk';
const sdk = new DeepdotsPopups();
export function DeepdotsHost({ children }: { children: React.ReactNode }) {
const [survey, setSurvey] = useState<ReactNativeSurveyPayload | null>(null);
const rendererRef = useRef<ReactNativePopupRenderer | null>(null);
useEffect(() => {
const renderer = new ReactNativePopupRenderer({
onShow: (payload) => setSurvey(payload),
onHide: () => setSurvey(null),
});
rendererRef.current = renderer;
// Sets the renderer, injects storage/device/platform, calls init()
// and wires AppState → onForeground/onBackground. Returns a cleanup fn.
return setupReactNative(
sdk,
{ apiKey: 'YOUR_PUBLIC_API_KEY', nodeEnv: __DEV__ ? 'development' : 'production' },
{
mmkv: new MMKV({ id: 'deepdots-sdk' }),
deviceInfo: DeviceInfo,
appState: AppState,
platform: Platform.OS === 'ios' ? 'ios' : 'android',
renderer,
},
);
}, []);
return (
<>
{children}
{survey ? (
<Modal visible transparent animationType="slide">
<View style={{ flex: 1 }}>
<WebView
style={{ flex: 1 }}
originWhitelist={['*']}
javaScriptEnabled
source={{ html: survey.html, baseUrl: 'https://sdk.deepdots.com/' }}
onMessage={(e) => rendererRef.current?.handleMessage(e.nativeEvent.data)}
/>
</View>
</Modal>
) : null}
</>
);
}

The renderer is a bridge, not a stub: onShow hands you { surveyId, productId, html } ready for <WebView source={{ html }}>, and handleMessage translates the WebView messages into SDK events — first interaction → popup_clicked (PARTIAL), completion → survey_completed (COMPLETED).

Rendering the survey without the SDK’s card (renderChrome)

Section titled “Rendering the survey without the SDK’s card (renderChrome)”

From 1.4.0, when you mount your own decorated container (a Modal, bottom sheet, or screen with its own card, background, rounded corners, or backdrop), pass renderChrome: false in the config:

setupReactNative(
sdk,
{ apiKey: 'YOUR_PUBLIC_API_KEY', renderChrome: false },
{ /* deps */ },
);

Since 1.3.0 the survey HTML draws its own card and backdrop (header with a close button, footer with the navigation buttons, rounded card, dimmed background). If your own container is also decorated, the two stack into a “double modal” — a card inside a card. renderChrome: false makes the WebView HTML transparent and edge-to-edge so it fills your container, while keeping the survey fully functional (message bridge, form, back/start/complete/send buttons, and the close button). You own the outer frame; the SDK owns the survey.

The flag only affects React Native (the survey WebView HTML). It has no effect on the web DOM popup, and it is ignored by the default <DeepdotsProvider>, whose Modal is already transparent and full-screen (so the built-in path shows a single card). Use it on the manual path shown above.

renderChrome decides who draws the frame. To restyle what is inside it — question wording, options, rating scales, and the SDK’s own header, progress bar and footer — pass your own stylesheet in surveyCss. It is injected as the last stylesheet in the WebView, so it wins the cascade without touching the defaults shared by every Deepdots customer.

That page also lists the class names to target. From 1.5.0 the SDK exposes the same hooks in both the web popup and this WebView, so one stylesheet covers both platforms.

DepTypeDefault when omitted
mmkvMMKV instanceconfig.storage, else in-memory
deviceInforeact-native-device-info moduleconfig.device, else omitted
appStateAppStateNo lifecycle wiring
platform'ios' / 'android' / 'web'config.platform, else 'web'
rendererPopupRendererThe SDK’s default renderer
errorUtilsglobal.ErrorUtilsglobalThis.ErrorUtils if present

Every dep is optional. Lower-level helpers are exported too if you want to build the pieces yourself: mmkvStorage(mmkv) (a synchronous KeyValueStorage adapter), collectRnDevice(deviceInfo), and buildSurveyHtml(options).