Lynx/Modules/Linking/Usage
@sigx/lynx-linking · Stable

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.

TSX
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:

TSX
try {
  await Linking.openURL('twitter://user?screen_name=sigx');
} catch {
  await Linking.openURL('https://twitter.com/sigx');
}

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.

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

While the app is running, incoming URLs arrive as events. Subscribe with addEventListener and keep the returned subscription so you can unsubscribe.

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

parse and createURL are pure JS, so they work in tests and SSR without a native bridge.

TSX
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 ?.

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.

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

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

TSX
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:

TSX
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 run sigx prebuild. The CLI adds the scheme to iOS CFBundleURLSchemes, adds an Android <intent-filter>, and wires the host to forward URLs into the module.
  • On iOS, canOpenURL silently returns false for non-http(s) schemes unless they are whitelisted in LSApplicationQueriesSchemes in Info.plist. Add mailto: and any third-party schemes you want to probe.
  • On Android, the Activity launchMode must be singleTop (or singleTask) so warm deep links reach the existing Activity via onNewIntent. The CLI templates set this.
  • Universal Links / App Links need extra host config (iOS Associated Domains + apple-app-site-association; Android autoVerify intent filter + assetlinks.json). Once set up, incoming https URLs flow through the same addEventListener('url', …) path.

Use Linking.isAvailable() to check at runtime whether the native bridge module is registered in the current build.

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:

SWIFT
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#