Receive Links in React Native
Handle incoming deep links in your React Native / Expo app using the @ulinkly/react-native SDK's event listeners. The SDK handles all native platform integration — you subscribe to events and route on the resolved data.
Prerequisites
SDK Integration
@ulinkly/react-nativeinstalled and native config applied (config plugin or manual)ULink.initialize(...)called and awaited at app startup- iOS configured (bundle ID, URL scheme, associated domains)
- Android configured (package name, intent filters)
Ulinkly Dashboard Configuration
- API key and domain configured for your project
If you haven't completed these steps, follow the React Native Getting Started guide.
Understanding Link Reception
The SDK exposes event listeners that fire with resolved link data when a user taps your link — on cold start, while the app is in the foreground, or after a deferred install. Each listener returns an EventSubscription; call .remove() when you no longer need it.
There are two primary channels:
onDynamicLink— dynamic links with parameters, for in-app navigation.onUnifiedLink— unified (platform-redirect) links; the SDK does not auto-navigate.
1. Listen to Link Events
Subscribe once, at a stable root of your app, inside a useEffect:
import { useEffect } from 'react';
import ULink, { ULinkResolvedData } from '@ulinkly/react-native';
export function DeepLinkObserver({ children }: { children: React.ReactNode }) {
useEffect(() => {
const dynSub = ULink.onDynamicLink((data) => handleDynamicLink(data));
const uniSub = ULink.onUnifiedLink((data) => handleUnifiedLink(data));
return () => {
dynSub.remove();
uniSub.remove();
};
}, []);
return <>{children}</>;
}
function handleDynamicLink(data: ULinkResolvedData) {
console.log('Dynamic link:', data.slug, data.parameters);
const screen = data.parameters?.screen as string | undefined;
if (screen) {
// navigate to `screen` using your router (see section 5)
}
}
function handleUnifiedLink(data: ULinkResolvedData) {
// Unified links are platform redirects — inspect data and open externally if appropriate.
console.log('Unified link:', data.slug);
}
If a link launches the app from a cold start, the SDK buffers it until two conditions are met: initialize() has resolved and your first listener has attached. As long as you call initialize() at startup and subscribe at app root, the cold-start link is flushed to your onDynamicLink / onUnifiedLink callback automatically — you don't need any special cold-start code path.
dispose()Call .remove() on each subscription in your cleanup to avoid leaks. Never call ULink.dispose() on component unmount or fast-refresh — it tears down the native SDK singleton (stops session tracking, unsubscribes native streams) for the lifetime of the process. Use dispose() only for an intentional full teardown, e.g. logout that reinitializes with a different API key.
2. Access Initial and Last Links
The event listeners are the preferred pattern, but pull-based accessors are available too.
Get the initial deep link
const initial = await ULink.getInitialDeepLink(); // ULinkResolvedData | null
if (initial) {
handleDynamicLink(initial);
}
Returns the link that launched the current session, or null if the app opened normally. (The same link is also delivered via the events above — use whichever pattern fits your app, but avoid handling it twice.)
Get the last link data
const last = await ULink.getLastLinkData(); // ULinkResolvedData | null
getLastLinkData() only returns data if you initialized with persistLastLinkData: true:
await ULink.initialize({ apiKey: 'your-api-key', persistLastLinkData: true });
3. Complete Example
import { useEffect } from 'react';
import ULink, { ULinkResolvedData } from '@ulinkly/react-native';
import { navigationRef } from './navigation'; // your navigation ref/helper
export function useDeepLinks() {
useEffect(() => {
const dynSub = ULink.onDynamicLink(handleLink);
const uniSub = ULink.onUnifiedLink((data) => {
// e.g. Linking.openURL(data.iosFallbackUrl ?? data.fallbackUrl)
console.log('Unified link:', data.slug);
});
return () => {
dynSub.remove();
uniSub.remove();
};
}, []);
}
function handleLink(data: ULinkResolvedData) {
if (data.isDeferred) {
console.log('Deferred deep link, match:', data.matchType);
}
const params = data.parameters ?? {};
switch (params.screen) {
case 'product':
if (params.productId) {
navigationRef.navigate('Product', { id: String(params.productId) });
}
break;
case 'profile':
if (params.userId) {
navigationRef.navigate('Profile', { id: String(params.userId) });
}
break;
default:
console.log('Unknown screen:', params.screen);
}
}
4. Deferred Deep Linking
Deferred deep linking delivers the original link to a user even if they didn't have your app installed when they tapped it: they click → install from the store → first launch resolves the original link, delivered through the same onDynamicLink listener with isDeferred: true.
By default the SDK checks for a deferred link on first launch (autoCheckDeferredLink). You can also trigger it explicitly:
await ULink.checkDeferredLink(); // results arrive via onDynamicLink / onUnifiedLink
| Property | Type | Description |
|---|---|---|
isDeferred | boolean | true if the link came from deferred deep linking |
matchType | string | "deterministic" (Android Install Referrer) or "probabilistic" (fingerprint) |
iOS: delay initialization to avoid the network-permission race
On iOS, the system shows a local-network/permission dialog on your app's first network request — and that first request is already in flight when the dialog appears, so it can fail regardless of the user's choice. If the SDK initializes immediately on a fresh install, the deferred-link check may fail before the user responds.
Recommended pattern: on iOS, delay initialize() until after onboarding or your first successful network call. On Android there's no such issue — initialize immediately.
import { Platform } from 'react-native';
import ULink from '@ulinkly/react-native';
async function initULink() {
await ULink.initialize({ apiKey: 'your-api-key', autoCheckDeferredLink: true });
}
// At startup: Android can init immediately.
if (Platform.OS === 'android') {
initULink().catch(console.error);
}
// On iOS, call initULink() after onboarding / first successful network request.
async function onOnboardingComplete() {
if (Platform.OS === 'ios') {
await initULink();
}
}
Matching methods
| Method | Platform | Accuracy | Notes |
|---|---|---|---|
| Install Referrer | Android | Deterministic | Exact click ID via Google Play |
| Fingerprint | iOS & Android | Probabilistic | IP + user-agent + timing; no IDFA/SKAdNetwork/ATT |
Deferred matching is fresh-install only, runs once per install, requires network, and click data expires 24 hours after the click. iOS matching is probabilistic — a 100% match rate cannot be guaranteed.
5. Integration with Routing
React Navigation
import { createNavigationContainerRef } from '@react-navigation/native';
export const navigationRef = createNavigationContainerRef();
function handleLink(data: ULinkResolvedData) {
const screen = data.parameters?.screen as string | undefined;
if (screen === 'product' && data.parameters?.productId) {
navigationRef.navigate('Product', { id: String(data.parameters.productId) });
}
}
Expo Router
import { router } from 'expo-router';
function handleLink(data: ULinkResolvedData) {
const productId = data.parameters?.productId;
if (productId) router.push(`/product/${productId}`);
}
Best Practices
- Subscribe at app root, once — a top-level
useEffect([])ensures cold-start links flush to your listener. - Always
.remove()subscriptions in cleanup; neverdispose()on unmount. - Handle both channels —
onDynamicLinkandonUnifiedLink. - Validate parameters — they're typed
Record<string, unknown>; coerce and null-check before use. - Centralize navigation — route all link handling through one function.
Troubleshooting
Links not opening the app
- Verify the URL scheme (iOS
Info.plist, Android intent filter) and Associated Domains / App Links host. - Ensure the domain matches the dashboard exactly.
- Test each platform separately.
Listeners not firing
- Confirm
initialize()resolved successfully (awaitit). - Make sure you subscribed at startup (cold-start links flush on first listener after init).
- Enable
debug: __DEV__and watch logs viaULink.onLog(...).
Parameters missing
- Confirm the link was created with parameters and you're reading the right channel (dynamic vs unified).
Query-parameter passthrough
If a link was created with allowQueryPassthrough: true, query params appended to the URL (e.g. ?orderId=123) are merged into data.parameters as strings, overriding stored params with the same key:
ULink.onDynamicLink((data) => {
const orderId = data.parameters?.orderId; // "123" (string)
});
See the Query-Parameter Passthrough guide.
Testing Tips
# Android (custom scheme or App Link)
adb shell am start -a android.intent.action.VIEW -d "https://myapp.shared.ly/your-slug"
# iOS Simulator
xcrun simctl openurl booted "https://myapp.shared.ly/your-slug"
- Test cold start (app not running) and warm start (app foregrounded).
- Build a dev client (
npx expo run:ios/run-android) — deep links don't work in Expo Go.
Next Steps
- Create Links → React Native — create links programmatically
- Troubleshoot & Test Deep Links — debug and verify