API expo-linking
Análisis de URLs, listeners y manejo de inicio en frío vs inicio en caliente - el manual de expo-linking para aplicaciones de Expo SDK 57 que reciben enlaces desde correo electrónico, SMS, códigos QR y otras aplicaciones.
Busca en todas las páginas de la documentación
Análisis de URLs, listeners y manejo de inicio en frío vs inicio en caliente - el manual de expo-linking para aplicaciones de Expo SDK 57 que reciben enlaces desde correo electrónico, SMS, códigos QR y otras aplicaciones.
Tarjeta de referencia rápida - lista para copiar y pegar.
import * as Linking from "expo-linking";
import { useEffect } from "react";
import { router } from "expo-router";
function routeFromUrl(url: string) {
const { path, queryParams } = Linking.parse(url);
const cleanPath = `/${(path ?? "").replace(/^\/+/, "")}`;
router.push({ pathname: cleanPath as never, params: queryParams ?? {} });
}
export function useDeepLinkBootstrap() {
useEffect(() => {
Linking.getInitialURL().then((url) => {
if (url) routeFromUrl(url);
});
const sub = Linking.addEventListener("url", ({ url }) => routeFromUrl(url));
return () => sub.remove();
}, []);
}Cuándo usarlo:
maps://, mailto:) con openURL.createURL.// src/linking/useAppLinking.ts
import * as Linking from "expo-linking";
import { router } from "expo-router";
import { useEffect, useRef } from "react";
import { z } from "zod";
const orderParams = z.object({
id: z.string().min(1),
ref: z.string().optional(),
});
function navigateFromUrl(url: string) {
const parsed = Linking.parse(url);
const segments = (parsed.path ?? "").split("/").filter(Boolean);
if (segments[0] === "orders" && segments[1]) {
const result = orderParams.safeParse({
id: segments[1],
ref: parsed.queryParams?.ref,
});
if (!result.success) {
router.replace("/");
return;
}
router.push({
pathname: "/orders/[id]",
params: { id: result.data.id, ref: result.data.ref ?? "" },
});
return;
}
router.replace("/");
}
type Options = {
enabled: boolean;
};
export function useAppLinking({ enabled }: Options) {
const handledInitial = useRef(false);
useEffect(() => {
if (!enabled) return;
Linking.getInitialURL().then((url) => {
if (!url || handledInitial.current) return;
handledInitial.current = true;
navigateFromUrl(url);
});
const subscription = Linking.addEventListener("url", ({ url }) => {
navigateFromUrl(url);
});
return () => subscription.remove();
}, [enabled]);
}// app/_layout.tsx
import { Stack } from "expo-router";
import { useAppLinking } from "../src/linking/useAppLinking";
import { useAuthReady } from "../src/auth/useAuthReady";
export default function RootLayout() {
const authReady = useAuthReady();
useAppLinking({ enabled: authReady });
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="orders/[id]" options={{ title: "Order" }} />
</Stack>
);
}Lo que esto demuestra:
getInitialURL con una guardia ref contra el manejo duplicado.addEventListener con limpieza al desmontar.authReady para que las rutas protegidas no parpadeen.router.push con pathname dinámico y params.getInitialURL la lee una vez que JavaScript comienza.url - la misma forma { url: string }.app/ - tu listener se ejecuta en paralelo para efectos secundarios.Linking.parse utiliza el analizador de URL WHATWG - las URLs no válidas devuelven { path: null, queryParams: null }.| Escenario | Estado de la aplicación | API | Trampa común |
|---|---|---|---|
| Inicio en frío | Proceso muerto | getInitialURL() | Navegar antes de que Auth/Router esté listo descarta la URL |
| Inicio en caliente (fondo) | JS ejecutándose | addEventListener("url") | Manejador duplicado si el layout se remonta sin limpieza |
| Inicio en caliente (primer plano) | JS ejecutándose | addEventListener("url") | La misma URL puede dispararse dos veces en algunos OEM de Android |
| Usuario abrió desde ícono | Sin enlace | getInitialURL() - null | No trates null como un error |
| Método / Evento | Devuelve | Uso |
|---|---|---|
Linking.parse(url) | { scheme, hostname, path, queryParams } | Inspeccionar URLs de entrada |
Linking.createURL(path, opts?) | string | Construir enlaces de compartir/referencia |
Linking.getInitialURL() | Promise<string | null> | Bootstrap de inicio en frío |
Linking.addEventListener("url", fn) | Subscription | Listener de inicio en caliente |
Linking.openURL(url) | Promise<true> | Abrir aplicaciones externas |
Linking.canOpenURL(url) | Promise<boolean> | Verificar si existe la aplicación destino |
Linking.getLinkingURL() | string | null | URL de Expo Go solo para desarrollo (evitar en prod) |
import * as Linking from "expo-linking";
type ParsedLink = ReturnType<typeof Linking.parse>;
function queryParam(
parsed: ParsedLink,
key: string
): string | undefined {
const value = parsed.queryParams?.[key];
return Array.isArray(value) ? value[0] : value;
}queryParams pueden ser string | string[] - normaliza antes del análisis con Zod.path omite la barra inclinada inicial para esquemas personalizados - normaliza antes de la concatenación.router.push cuando experiments.typedRoutes está habilitado - las rutas no válidas fallan en tiempo de compilación.useAppLinking({ enabled: authReady }) como se muestra arriba.initialRouteName explícitamente.return () => subscription.remove() en useEffect.myapp:// de forma rígida - rompe entre sabores y Expo Go. Solución: siempre usa Linking.createURL("orders/42").?token= en URLs aterrizaba en analytics y logs. Solución: intercambia tokens del lado del servidor; usa códigos de una sola vez de corta duración en enlaces.canOpenURL bloqueado en iOS sin LSApplicationQueriesSchemes - siempre devuelve false para esquemas no listados. Solución: declara esquemas consultados en app.config.ts ios.infoPlist.| Alternativa | Usar cuando | No usar cuando |
|---|---|---|
| Vinculación de Expo Router solamente | Las rutas basadas en archivos cubren todas las rutas | Necesitas análisis previo a la navegación o intercambio de tokens |
Configuración de linking de React Navigation | Aplicación de React Navigation marrón | Proyecto Expo Router verde |
| SDK de Branch / AppsFlyer | Enlaces profundos diferidos + atribución | Aplicación simple de esquema personalizado solamente |
Linking.parse manual en cada pantalla | Base de código heredada | Código nuevo - centralizar en un hook |
expo-linking para createURL, openURL, canOpenURL y lógica personalizada previa a la navegación._layout.tsx raíz o un proveedor de autenticación.const url = "maps://?q=Coffee+Shop";
if (await Linking.canOpenURL(url)) {
await Linking.openURL(url);
}LSApplicationQueriesSchemes en iOS.parse y createURL funcionan en la web con orígenes http/https.getInitialURL devuelve la URL de la página en la primera carga - protege con Platform.OS !== "web" si compartes código.?tag=a&tag=b produce queryParams.tag como string[].path - elimina las claves de consulta token, email y code antes de analytics.https:// disparan el mismo evento una vez que se abre la aplicación.jest.spyOn(Linking, "getInitialURL").mockResolvedValue("shopapp://orders/1");addEventListener para invocar la devolución de llamada manualmente en pruebas de integración.Versiones de Stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0, y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026