Mr. Doge
Guides

React Native

Mobile app integration with token persistence and screen-aware subscriptions.

@mrdoge/client works out of the box on React Native (Expo and bare). Same JWT-based auth pattern as the browser.

Install

npm i @mrdoge/react

@mrdoge/react wraps @mrdoge/client with hooks and a shared cache, recommended even on RN, for the same reason as the browser: components watching the same match/list don't duplicate a subscription.

If you need to persist auth tokens across app launches, add AsyncStorage:

npx expo install @react-native-async-storage/async-storage

Mint tokens from your backend

Whatever backend you use (Next.js, Express, your own), expose a /mrdoge/token endpoint that calls tokens.create. See the Next.js guide for an example.

Client setup

Call configureMrDoge() once, before any hook runs, e.g. at your app's entry point:

lib/mrdoge.ts
import { configureMrDoge } from "@mrdoge/react";

configureMrDoge({
  fetchToken: async () => {
    const res = await fetch("https://your-api.com/mrdoge/token", {
      method: "POST",
      headers: {
        // forward your app's session/JWT
        Authorization: `Bearer ${await getAppToken()}`,
      },
    });
    if (!res.ok) throw new Error("auth failed");
    return res.json();
  },
});

The custom fetchToken lets you forward your app's session and gate the Mr. Doge token mint by your own auth. Same options as MrDogeOptions in @mrdoge/client.

Persist the token (optional)

For warm starts, cache the last token in AsyncStorage:

import AsyncStorage from "@react-native-async-storage/async-storage";
import { configureMrDoge } from "@mrdoge/react";

configureMrDoge({
  fetchToken: async () => {
    // try cache first
    const cached = await AsyncStorage.getItem("mrdoge.token");
    if (cached) {
      const parsed = JSON.parse(cached);
      if (Date.parse(parsed.expiresAt) - Date.now() > 30_000) {
        return parsed;
      }
    }

    // otherwise mint a fresh one
    const res = await fetch("https://your-api.com/mrdoge/token", {
      method: "POST",
    });
    const fresh = await res.json();
    await AsyncStorage.setItem("mrdoge.token", JSON.stringify(fresh));
    return fresh;
  },
});

Saves a round-trip on cold start. The SDK still refreshes mid-flight if the token expires.

Screen-aware subscription

screens/LiveScreen.tsx
import { View, Text } from "react-native";
import { useLiveMatches } from "@mrdoge/react";

export function LiveScreen() {
  const matches = useLiveMatches({ sports: ["soccer"] });

  return (
    <View>
      {(matches ?? []).map((m) => (
        <Text key={m.id}>
          {m.homeTeam.name} {m.stats?.homeScore}-{m.stats?.awayScore}{" "}
          {m.awayTeam.name}
        </Text>
      ))}
    </View>
  );
}

Mobile apps churn screens hard: useLiveMatches cancels the underlying subscription automatically once the last component using it unmounts, no manual cleanup to get wrong.

Manual control

Reach for getMrDogeClient() (the same configured client the hooks use) when you need more than a hook gives you:

screens/LiveScreen.tsx
import { useEffect, useState } from "react";
import { View, Text } from "react-native";
import { getMrDogeClient } from "@mrdoge/react";
import type { Match, Subscription } from "@mrdoge/client";

export function LiveScreen() {
  const [matches, setMatches] = useState<Match[]>([]);

  useEffect(() => {
    let cancelled = false;
    let sub: Subscription<"matches.subscribeLive"> | null = null;

    (async () => {
      sub = await getMrDogeClient().matches.subscribeLive({ sports: ["soccer"] });
      if (cancelled) {
        sub.cancel();
        return;
      }
      setMatches(sub.snapshot);

      sub.on("match.upd", (match) => {
        setMatches((prev) => {
          const i = prev.findIndex((m) => m.id === match.id);
          if (i >= 0) {
            const next = [...prev];
            next[i] = match;
            return next;
          }
          return [...prev, match];
        });
      });

      sub.on("match.del", ({ id }) => {
        setMatches((prev) => prev.filter((m) => m.id !== id));
      });
    })();

    return () => {
      cancelled = true;
      sub?.cancel();
    };
  }, []);

  return (
    <View>
      {matches.map((m) => (
        <Text key={m.id}>
          {m.homeTeam.name} {m.stats?.homeScore}-{m.stats?.awayScore}{" "}
          {m.awayTeam.name}
        </Text>
      ))}
    </View>
  );
}

Background and reconnect

When the app backgrounds, iOS / Android may close the WebSocket. The SDK auto-reconnects on its own; your sub.on() listeners keep firing once the socket is back.

For best foreground UX, wire AppState to pingOrReconnect():

lib/mrdoge.ts
import { AppState } from "react-native";
import { getMrDogeClient } from "@mrdoge/react";

AppState.addEventListener("change", (state) => {
  if (state === "active") getMrDogeClient().pingOrReconnect();
});

pingOrReconnect():

  • No-ops when the socket is healthy.
  • Fires a reconnect when the socket is dead but no reconnect is in flight (e.g. iOS killed the WS during background and the loop hadn't picked it up yet).
  • Wakes the SDK's backoff loop when one is currently sleeping, so the first user interaction after foreground doesn't wait out a stale exponential delay (could be 30s+ on a long-failed reconnect series).

Never throws. Safe to call on every AppState change event.

For long backgrounds (>10 minutes), the JWT may expire mid-reconnect. The SDK refetches via your fetchToken callback transparently.

TanStack Query interop

@mrdoge/react's own hooks already share a cache. Reach for TanStack Query instead only if you want Mr. Doge data unified into a cache layer you're already using for everything else:

import { useQuery } from "@tanstack/react-query";
import { getMrDogeClient } from "@mrdoge/react";

export function useMatches(sport: string) {
  return useQuery({
    queryKey: ["matches", sport],
    queryFn: () => getMrDogeClient().matches.list({ sports: [sport], limit: 50 }),
    staleTime: 60_000,
  });
}

The SDK doesn't fight your cache layer: every method is a plain async function.

Next

On this page