Skip to main content

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-native installed 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.

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.

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);
}
Cold-start links are buffered for you

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.

Clean up, but do not 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.

The event listeners are the preferred pattern, but pull-based accessors are available too.

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.)

const last = await ULink.getLastLinkData(); // ULinkResolvedData | null
Persistence required

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
PropertyTypeDescription
isDeferredbooleantrue if the link came from deferred deep linking
matchTypestring"deterministic" (Android Install Referrer) or "probabilistic" (fingerprint)

iOS: delay initialization to avoid the network-permission race

iOS network permission dialog

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

MethodPlatformAccuracyNotes
Install ReferrerAndroidDeterministicExact click ID via Google Play
FingerprintiOS & AndroidProbabilisticIP + 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; never dispose() on unmount.
  • Handle both channelsonDynamicLink and onUnifiedLink.
  • Validate parameters — they're typed Record<string, unknown>; coerce and null-check before use.
  • Centralize navigation — route all link handling through one function.

Troubleshooting

  • 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 (await it).
  • Make sure you subscribed at startup (cold-start links flush on first listener after init).
  • Enable debug: __DEV__ and watch logs via ULink.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