API expo-linking
Análise de URLs, listeners e tratamento de inicialização a frio vs. inicialização aquecida - o cookbook expo-linking para aplicativos Expo SDK 57 que recebem links de e-mail, SMS, códigos QR e outros aplicativos.
Busque em todas as páginas da documentação
Análise de URLs, listeners e tratamento de inicialização a frio vs. inicialização aquecida - o cookbook expo-linking para aplicativos Expo SDK 57 que recebem links de e-mail, SMS, códigos QR e outros aplicativos.
Cartão de receita de referência rápida - pronto para copiar e colar.
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();
}, []);
}Quando usar isso:
maps://, mailto:) com 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>
);
}O que isso demonstra:
getInitialURL com um ref para evitar tratamento duplo.addEventListener com limpeza na desmontagem.authReady para que rotas protegidas não pisquem.router.push do Expo Router com pathname e parâmetros dinâmicos.getInitialURL a lê assim que o JavaScript é iniciado.url - a mesma forma { url: string }.app/ - seu listener é executado em paralelo para efeitos colaterais.Linking.parse usa o analisador de URL WHATWG - URLs inválidas retornam { path: null, queryParams: null }.| Cenário | Estado do App | API | Armadilha |
|---|---|---|---|
| Inicialização a frio | Processo encerrado | getInitialURL() | Navegar antes que a autenticação/Router esteja pronto descarta a URL |
| Inicialização aquecida (em segundo plano) | JS em execução | addEventListener("url") | Manipulador duplicado se o layout for remontado sem limpeza |
| Inicialização aquecida (em primeiro plano) | JS em execução | addEventListener("url") | A mesma URL pode disparar duas vezes em alguns OEMs Android |
| Usuário abriu pelo ícone | Nenhum link | getInitialURL() → null | Não trate null como um erro |
| Método / Evento | Retorna | Uso |
|---|---|---|
Linking.parse(url) | { scheme, hostname, path, queryParams } | Inspecionar URLs de entrada |
Linking.createURL(path, opts?) | string | Construir links de compartilhamento/referência |
Linking.getInitialURL() | Promise<string | null> | Inicialização a frio |
Linking.addEventListener("url", fn) | Subscription | Listener de inicialização aquecida |
Linking.openURL(url) | Promise<true> | Abrir aplicativos externos |
Linking.canOpenURL(url) | Promise<boolean> | Verificar se o aplicativo de destino existe |
Linking.getLinkingURL() | string | null | URL do Expo Go apenas para desenvolvimento (evitar em produção) |
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 podem ser string | string[] - normalize antes da análise Zod.path omite a barra inicial para esquemas personalizados - normalize antes da concatenação.router.push quando experiments.typedRoutes estiver habilitado - caminhos inválidos falham em tempo de compilação.useAppLinking({ enabled: authReady }) como mostrado acima.initialRouteName explicitamente.return () => subscription.remove() em useEffect.myapp:// - quebra entre sabores e Expo Go. Correção: sempre use Linking.createURL("orders/42").?token= em URLs aparece em análises e logs. Correção: troque tokens no lado do servidor; use códigos de uso único de curta duração em links.canOpenURL bloqueado no iOS sem LSApplicationQueriesSchemes - sempre retorna false para esquemas não listados. Correção: declare os esquemas consultados em app.config.ts ios.infoPlist.| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| Apenas linking do Expo Router | Rotas baseadas em arquivos cobrem todos os caminhos | Você precisa de análises pré-navegação ou troca de tokens |
| Configuração de linking do React Navigation | Aplicativo React Navigation Brownfield | Projeto Expo Router Greenfield |
| SDK Branch / AppsFlyer | Links profundos adiados + atribuição | Aplicativo simples apenas com esquema personalizado |
Linking.parse manual em cada tela | Base de código legada | Código novo - centralizar em um único hook |
expo-linking para createURL, openURL, canOpenURL e lógica personalizada pré-navegação._layout.tsx raiz ou em um provedor de autenticação.const url = "maps://?q=Coffee+Shop";
if (await Linking.canOpenURL(url)) {
await Linking.openURL(url);
}LSApplicationQueriesSchemes no iOS.parse e createURL funcionam na web com origens http/https.getInitialURL retorna a URL da página na primeira carga - controle com Platform.OS !== "web" se estiver compartilhando código.?tag=a&tag=b resulta em queryParams.tag como string[].path - remova as chaves de consulta token, email e code antes da análise.https:// verificados disparam o mesmo evento assim que o aplicativo abre.jest.spyOn(Linking, "getInitialURL").mockResolvedValue("shopapp://orders/1");addEventListener para invocar o callback manualmente em testes de integração.Versões do Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026