📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
---
|
||||
name: native-data-fetching
|
||||
description: Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (useLoaderData).
|
||||
description: Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (`useLoaderData`).
|
||||
risk: unknown
|
||||
source: community
|
||||
version: 1.0.0
|
||||
source: https://github.com/expo/skills/tree/main/plugins/expo/skills/native-data-fetching
|
||||
source_repo: expo/skills
|
||||
source_type: official
|
||||
date_added: 2026-07-01
|
||||
license: MIT
|
||||
license_source: https://github.com/expo/skills/blob/main/LICENSE
|
||||
---
|
||||
|
||||
# Expo Networking
|
||||
@@ -21,6 +24,7 @@ references/
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Implementing API requests
|
||||
@@ -497,10 +501,8 @@ User: "How do I handle authentication tokens?"
|
||||
|
||||
User: "API calls are slow"
|
||||
-> Check caching strategy, use React Query staleTime
|
||||
|
||||
User: "How do I configure different API URLs for dev and prod?"
|
||||
-> Use EXPO*PUBLIC* env vars with .env.development and .env.production files
|
||||
|
||||
User: "Where should I put my API key?"
|
||||
-> Client-safe keys: EXPO*PUBLIC* in .env. Secret keys: non-prefixed env vars in API routes only
|
||||
|
||||
@@ -508,6 +510,7 @@ User: "How do I load data for a page in Expo Router?"
|
||||
-> See references/expo-router-loaders.md for route-level loaders (web, SDK 55+). For native, use React Query or fetch.
|
||||
|
||||
## Limitations
|
||||
- Use this skill only when the task clearly matches the scope described above.
|
||||
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||
|
||||
- Use this skill only when the task clearly matches its upstream product or API scope.
|
||||
- Verify commands, API behavior, pricing, quotas, credentials, and deployment effects against current official documentation before making changes.
|
||||
- Do not treat generated examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Native Data Fetching"
|
||||
short_description: "Implement and debug Expo network requests, caching, auth, offline support, and Expo Router data loaders"
|
||||
default_prompt: "Use $native-data-fetching for Expo data fetching work: fetch wrappers, React Query or SWR setup, error handling, retry/cancellation, authenticated requests, environment-based API URLs, offline behavior, and Expo Router loaders."
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
# Expo Router Data Loaders
|
||||
|
||||
Route-level data loading for web apps using Expo SDK 55+. Loaders are async functions exported from route files that load data before the route renders, following the Remix/React Router loader model.
|
||||
|
||||
**Dual execution model:**
|
||||
|
||||
- **Initial page load (SSR):** The loader runs server-side. Its return value is serialized as JSON and embedded in the HTML response.
|
||||
- **Client-side navigation:** The browser fetches the loader data from the server via HTTP. The route renders once the data arrives.
|
||||
|
||||
You write one function and the framework manages when and how it executes.
|
||||
|
||||
## Configuration
|
||||
|
||||
**Requirements:** Expo SDK 55+, web output mode (`npx expo serve` or `npx expo export --platform web`) set in `app.json` or `app.config.js`.
|
||||
|
||||
**Server rendering:**
|
||||
|
||||
```json
|
||||
{
|
||||
"expo": {
|
||||
"web": {
|
||||
"output": "server"
|
||||
},
|
||||
"plugins": [
|
||||
["expo-router", {
|
||||
"unstable_useServerDataLoaders": true,
|
||||
"unstable_useServerRendering": true
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Static/SSG:**
|
||||
|
||||
```json
|
||||
{
|
||||
"expo": {
|
||||
"web": {
|
||||
"output": "static"
|
||||
},
|
||||
"plugins": [
|
||||
["expo-router", {
|
||||
"unstable_useServerDataLoaders": true
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| | `"server"` | `"static"` |
|
||||
|---|-----------|------------|
|
||||
| `unstable_useServerDataLoaders` | Required | Required |
|
||||
| `unstable_useServerRendering` | Required | Not required |
|
||||
| Loader runs on | Live server (every request) | Build time (static generation) |
|
||||
| `request` object | Full access (headers, cookies) | Not available |
|
||||
| Hosting | Node.js server (EAS Hosting) | Any static host (Netlify, Vercel, S3) |
|
||||
|
||||
## Imports
|
||||
|
||||
Loaders use two packages:
|
||||
|
||||
- **`expo-router`** — `useLoaderData` hook
|
||||
- **`expo-server`** — `LoaderFunction` type, `StatusError`, `setResponseHeaders`. Always available (dependency of `expo-router`), no install needed.
|
||||
|
||||
## Basic Loader
|
||||
|
||||
For loaders without params, a plain async function works:
|
||||
|
||||
```tsx
|
||||
// app/posts/index.tsx
|
||||
import { Suspense } from "react";
|
||||
import { useLoaderData } from "expo-router";
|
||||
import { ActivityIndicator, View, Text } from "react-native";
|
||||
|
||||
export async function loader() {
|
||||
const response = await fetch("https://api.example.com/posts");
|
||||
const posts = await response.json();
|
||||
return { posts };
|
||||
}
|
||||
|
||||
function PostList() {
|
||||
const { posts } = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<View>
|
||||
{posts.map((post) => (
|
||||
<Text key={post.id}>{post.title}</Text>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Posts() {
|
||||
return (
|
||||
<Suspense fallback={<ActivityIndicator size="large" />}>
|
||||
<PostList />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`useLoaderData` is typed via `typeof loader` — the generic parameter infers the return type.
|
||||
|
||||
## Dynamic Routes
|
||||
|
||||
For loaders with params, use the `LoaderFunction<T>` type from `expo-server`. The first argument is the request (an immutable `Request`-like object, or `undefined` in static mode). The second is `params` (`Record<string, string | string[]>`), which contains **path parameters only**. Access individual params with a cast like `params.id as string`. For query parameters, use `new URL(request.url).searchParams`:
|
||||
|
||||
```tsx
|
||||
// app/posts/[id].tsx
|
||||
import { Suspense } from "react";
|
||||
import { useLoaderData } from "expo-router";
|
||||
import { StatusError, type LoaderFunction } from "expo-server";
|
||||
import { ActivityIndicator, View, Text } from "react-native";
|
||||
|
||||
type Post = {
|
||||
id: number;
|
||||
title: string;
|
||||
body: string;
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction<{ post: Post }> = async (
|
||||
request,
|
||||
params,
|
||||
) => {
|
||||
const id = params.id as string;
|
||||
const response = await fetch(`https://api.example.com/posts/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new StatusError(404, `Post ${id} not found`);
|
||||
}
|
||||
|
||||
const post: Post = await response.json();
|
||||
return { post };
|
||||
};
|
||||
|
||||
function PostContent() {
|
||||
const { post } = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>{post.title}</Text>
|
||||
<Text>{post.body}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PostDetail() {
|
||||
return (
|
||||
<Suspense fallback={<ActivityIndicator size="large" />}>
|
||||
<PostContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Catch-all routes access `params.slug` the same way:
|
||||
|
||||
```tsx
|
||||
// app/docs/[...slug].tsx
|
||||
import { type LoaderFunction } from "expo-server";
|
||||
|
||||
type Doc = { title: string; content: string };
|
||||
|
||||
export const loader: LoaderFunction<{ doc: Doc }> = async (request, params) => {
|
||||
const slug = params.slug as string[];
|
||||
const path = slug.join("/");
|
||||
const doc = await fetchDoc(path);
|
||||
return { doc };
|
||||
};
|
||||
```
|
||||
|
||||
Query parameters are available via the `request` object (server output mode only):
|
||||
|
||||
```tsx
|
||||
// app/search.tsx
|
||||
import { type LoaderFunction } from "expo-server";
|
||||
|
||||
export const loader: LoaderFunction<{ results: any[]; query: string }> = async (request) => {
|
||||
// Assuming request.url is `/search?q=expo&page=2`
|
||||
const url = new URL(request!.url);
|
||||
const query = url.searchParams.get("q") ?? "";
|
||||
const page = Number(url.searchParams.get("page") ?? "1");
|
||||
|
||||
const results = await fetchSearchResults(query, page);
|
||||
return { results, query };
|
||||
};
|
||||
```
|
||||
|
||||
## Server-Side Secrets & Request Access
|
||||
|
||||
Loaders run on the server, so you can access secrets and server-only resources directly:
|
||||
|
||||
```tsx
|
||||
// app/dashboard.tsx
|
||||
import { type LoaderFunction } from "expo-server";
|
||||
|
||||
export const loader: LoaderFunction<{ balance: any; isAuthenticated: boolean }> = async (
|
||||
request,
|
||||
params,
|
||||
) => {
|
||||
const data = await fetch("https://api.stripe.com/v1/balance", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
|
||||
},
|
||||
});
|
||||
|
||||
const sessionToken = request?.headers.get("cookie")?.match(/session=([^;]+)/)?.[1];
|
||||
|
||||
const balance = await data.json();
|
||||
return { balance, isAuthenticated: !!sessionToken };
|
||||
};
|
||||
```
|
||||
|
||||
The `request` object is available in server output mode. In static output mode, `request` is always `undefined`.
|
||||
|
||||
## Response Utilities
|
||||
|
||||
### Setting Response Headers
|
||||
|
||||
```tsx
|
||||
// app/products.tsx
|
||||
import { setResponseHeaders } from "expo-server";
|
||||
|
||||
export async function loader() {
|
||||
setResponseHeaders({
|
||||
"Cache-Control": "public, max-age=300",
|
||||
});
|
||||
|
||||
const products = await fetchProducts();
|
||||
return { products };
|
||||
}
|
||||
```
|
||||
|
||||
### Throwing HTTP Errors
|
||||
|
||||
```tsx
|
||||
// app/products/[id].tsx
|
||||
import { StatusError, type LoaderFunction } from "expo-server";
|
||||
|
||||
export const loader: LoaderFunction<{ product: Product }> = async (request, params) => {
|
||||
const id = params.id as string;
|
||||
const product = await fetchProduct(id);
|
||||
|
||||
if (!product) {
|
||||
throw new StatusError(404, "Product not found");
|
||||
}
|
||||
|
||||
return { product };
|
||||
};
|
||||
```
|
||||
|
||||
## Suspense & Error Boundaries
|
||||
|
||||
### Loading States with Suspense
|
||||
|
||||
`useLoaderData()` suspends during client-side navigation. Push it into a child component and wrap with `<Suspense>`:
|
||||
|
||||
```tsx
|
||||
// app/posts/index.tsx
|
||||
import { Suspense } from "react";
|
||||
import { useLoaderData } from "expo-router";
|
||||
import { ActivityIndicator, View, Text } from "react-native";
|
||||
|
||||
export async function loader() {
|
||||
const response = await fetch("https://api.example.com/posts");
|
||||
return { posts: await response.json() };
|
||||
}
|
||||
|
||||
function PostList() {
|
||||
const { posts } = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<View>
|
||||
{posts.map((post) => (
|
||||
<Text key={post.id}>{post.title}</Text>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Posts() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<ActivityIndicator size="large" />
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<PostList />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The `<Suspense>` boundary must be above the component calling `useLoaderData()`. On initial page load the data is already in the HTML, suspension only occurs during client-side navigation.
|
||||
|
||||
### Error Boundaries
|
||||
|
||||
```tsx
|
||||
// app/posts/[id].tsx
|
||||
export function ErrorBoundary({ error }: { error: Error }) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<Text>Error: {error.message}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
When a loader throws (including `StatusError`), the nearest `ErrorBoundary` catches it.
|
||||
|
||||
## Static vs Server Rendering
|
||||
|
||||
| | Server (`"server"`) | Static (`"static"`) |
|
||||
|---|---|---|
|
||||
| **When loader runs** | Every request (live) | At build time (`npx expo export`) |
|
||||
| **Data freshness** | Fresh on initial server request | Stale until next build |
|
||||
| **`request` object** | Full access | Not available |
|
||||
| **Hosting** | Node.js server (EAS Hosting) | Any static host |
|
||||
| **Use case** | Personalized/dynamic content | Marketing pages, blogs, docs |
|
||||
|
||||
**Choose server** when data changes frequently or content is personalized (cookies, auth, headers).
|
||||
|
||||
**Choose static** when content is the same for all users and changes infrequently.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Loaders are web-only; use client-side fetching (React Query, fetch) for native
|
||||
- Loaders cannot be used in `_layout` files — only in route files
|
||||
- Use `LoaderFunction<T>` from `expo-server` to type loaders that use params
|
||||
- The request object is immutable — use optional chaining (`request?.headers`) as it may be `undefined` in static mode
|
||||
- Return only JSON-serializable values (no `Date`, `Map`, `Set`, class instances, functions)
|
||||
- Use non-prefixed `process.env` vars for secrets in loaders, not `EXPO_PUBLIC_` (which is embedded in the client bundle)
|
||||
- Use `StatusError` from `expo-server` for HTTP error responses
|
||||
- Use `setResponseHeaders` from `expo-server` to set headers
|
||||
- Export `ErrorBoundary` from route files to handle loader failures gracefully
|
||||
- Validate and sanitize user input (params, query strings) before using in database queries or API calls
|
||||
- Handle errors gracefully with try/catch; log server-side for debugging
|
||||
- Loader data is currently cached for the session. This is a known limitation that will be lifted in a future release
|
||||
Reference in New Issue
Block a user