Using Linking
Open outbound URLs, read the URL that launched your app, and react to deep links while it runs.
Basic usage
Import the Linking facade and call its static methods. openURL and canOpenURL cross the native bridge and return promises; getInitialURL reads a cached value synchronously.
import { Linking } from '@sigx/lynx-linking';
// Open a URL with the system handler (browser, mail, dialer).
await Linking.openURL('https://lynxjs.org');
// Probe whether a scheme can be handled before offering it.
if (await Linking.canOpenURL('mailto:hi@example.com')) {
await Linking.openURL('mailto:hi@example.com');
}
openURL rejects with a SigxError (code: 'native_error') when the OS refuses to open the URL — it is invalid, or no installed app handles the scheme. That makes the try-the-app, fall-back-to-the-web pattern work:
try {
await Linking.openURL('twitter://user?screen_name=sigx');
} catch {
await Linking.openURL('https://twitter.com/sigx');
}
Handling cold-start deep links
When the app is launched from a closed state by a deep link, the launch URL is available synchronously from getInitialURL. Read it once at startup; it returns null when the app started normally.
import { Linking } from '@sigx/lynx-linking';
const initial = Linking.getInitialURL();
if (initial) {
handleDeepLink(initial);
}
There is no bridge round-trip here — the value comes from a global the native host populates before your bundle runs, so it is safe to call during initial render.
Reacting to warm deep links
While the app is running, incoming URLs arrive as events. Subscribe with addEventListener and keep the returned subscription so you can unsubscribe.
import { Linking } from '@sigx/lynx-linking';
import { onMounted, onUnmounted } from 'sigx';
onMounted(() => {
const sub = Linking.addEventListener('url', ({ url }) => {
handleDeepLink(url);
});
onUnmounted(() => sub.remove());
});
The only valid event type is 'url'; passing anything else throws. On the web, SSR, or test environments — where there is no Lynx native host — addEventListener returns a no-op subscription, so this code is safe to run anywhere.
Parsing and building deep-link URLs
parse and createURL are pure JS, so they work in tests and SSR without a native bridge.
import { parse, createURL } from '@sigx/lynx-linking';
const { scheme, hostname, path, queryParams } =
parse('myapp://user/42?ref=share');
// scheme: 'myapp', hostname: 'user', path: '/42', queryParams: { ref: 'share' }
const link = createURL('/profile/42', { ref: 'share' });
// '/profile/42?ref=share'
parse lowercases the scheme, strips fragments, and percent-decodes query values (treating + as a space). A bare path (no scheme) is returned untouched in path and its query is not split out. createURL skips undefined/null values and joins with & if the path already contains a ?.
Wiring deep links into the router
If you use @sigx/router, the useLinkingRouter composable wires both cold-start and warm deep links straight into navigation. Import it from the /router subpath and call it once at your app root.
import { useLinkingRouter } from '@sigx/lynx-linking/router';
function App() {
useLinkingRouter({
prefixes: ['myapp://', 'https://your.domain'],
});
// ...
}
It reads the initial URL on mount, subscribes for warm URLs, strips the first matching prefix, and calls router.push() with the remaining path and query. The listener is removed automatically on unmount. Pass onURL instead of relying on the default to handle URLs yourself (for example, an auth callback) — that skips the router push entirely.
useLinkingRouter({ onURL: (url) => myAuthCallback(url) });
This subpath has optional peer dependencies on @sigx/router and sigx.
Android hardware back button
BackHandler handles the Android hardware back press and lets you send the app to the background. Both behaviors are Android-only at runtime — on iOS the listener never fires and exitApp rejects.
import { BackHandler } from '@sigx/lynx-linking';
const sub = BackHandler.addEventListener(() => {
if (router.canGoBack) {
router.pop();
return true; // handled
}
return false; // let the default back behavior run
});
// Later, to detach:
sub.remove();
To back out of the app while keeping the JS bundle warm:
await BackHandler.exitApp().catch(() => {
// rejects on iOS, and on Android with no hosting Activity
});
exitApp calls moveTaskToBack rather than terminating the process. On iOS it always rejects with a SigxError (code: 'native_error') — Apple forbids programmatic termination. Always consume the rejection: an unhandled one is fatal on Lynx, not a warning.
Native setup notes
Deep links require host configuration beyond installing the package:
- Declare your scheme in
signalx.config.ts(e.g.scheme: 'myapp') and runsigx prebuild. The CLI adds the scheme to iOSCFBundleURLSchemes, adds an Android<intent-filter>, and wires the host to forward URLs into the module. - On iOS,
canOpenURLsilently returnsfalsefor non-http(s)schemes unless they are whitelisted inLSApplicationQueriesSchemesinInfo.plist. Addmailto:and any third-party schemes you want to probe. - On Android, the Activity
launchModemust besingleTop(orsingleTask) so warm deep links reach the existing Activity viaonNewIntent. The CLI templates set this. - Universal Links / App Links need extra host config (iOS Associated Domains +
apple-app-site-association; AndroidautoVerifyintent filter +assetlinks.json). Once set up, incominghttpsURLs flow through the sameaddEventListener('url', …)path.
Use Linking.isAvailable() to check at runtime whether the native bridge module is registered in the current build.
iOS: forward scene deep links
The generated iOS app is a SwiftUI scene app. A scene app never receives application(_:open:) or a launch-options URL — the scene gets the URL instead. The App.swift that sigx prebuild writes forwards both scene callbacks to the package hooks, so cold and warm deep links, and Universal Links, reach @sigx/lynx-linking:
WindowGroup {
ContentView(devUrl: Self.getDevUrl())
.onOpenURL { url in
_ = GeneratedAppDelegateHooks.openURL(url, options: [:])
}
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
GeneratedAppDelegateHooks.continueUserActivity(activity)
}
}
Without these two modifiers, getInitialURL() returns null and no url event fires on iOS. App.swift is a managed file: sigx prebuild with @sigx/lynx-cli 0.33.0 or later rewrites it from the template, so an existing ios/ project picks up the forwarding on its next prebuild. If you maintain ios/<AppName>/App.swift by hand and don't let prebuild refresh it, add the two modifiers to the ContentView inside your WindowGroup as shown.
See also
- Installation — package and native setup.
- API reference — every export.
