Solid Router data APIs: query for caching/deduplication, createAsync for reactive promises, createAsyncStore for store-based async, revalidate for cache invalidation.
Complete guide to data fetching and caching in Solid Router. Use these APIs for efficient data fetching, caching, and cache invalidation.
query wraps an async function and provides automatic caching and deduplication. Prevents redundant API calls and enables efficient data fetching.
import { query } from "@solidjs/router";
const getUserProfileQuery = query(async (userId: string) => {
const response = await fetch(
`https://api.example.com/users/${encodeURIComponent(userId)}`
);
const json = await response.json();
if (!response.ok) {
throw new Error(json?.message ?? "Failed to load user profile.");
}
return json;
}, "userProfile");
Key characteristics:
The cached result is reused in these scenarios:
const getUserQuery = query(async (id: string) => {
// ...
}, "users");
getUserQuery.key; // "users"
getUserQuery.keyFor("123"); // "users[123]"
Keys are generated from:
createAsync transforms promises into reactive signals. Recommended for most async data fetching.
import { createAsync, query } from "@solidjs/router";
import { Suspense, ErrorBoundary } from "solid-js";
const getUserQuery = query(async (id: string) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
}, "user");
function UserProfile() {
const user = createAsync(() => getUserQuery("123"));
return (
<ErrorBoundary fallback={<p>Could not fetch user data.</p>}>
<Suspense fallback={<p>Loading user...</p>}>
<p>{user()!.name}</p>
</Suspense>
</ErrorBoundary>
);
}
const data = createAsync(
() => fetchData(),
{
name: "myData", // Debug name
initialValue: null, // Initial value before fetch
deferStream: false, // Defer streaming until resolved
}
);
Options:
name: Debug identifierinitialValue: Value before first fetch completesdeferStream: Defer streaming SSR until resolvedfunction UserProfile() {
const [userId, setUserId] = createSignal("123");
// Re-fetches when userId changes
const user = createAsync(() => getUserQuery(userId()));
return (
<div>
<button onClick={() => setUserId("456")}>Switch User</button>
<Suspense fallback={<p>Loading...</p>}>
<p>{user()?.name}</p>
</Suspense>
</div>
);
}
createAsyncStore creates deeply reactive stores from async data. Uses reconciliation to merge updates intelligently.
import { createAsyncStore, query } from "@solidjs/router";
import { For } from "solid-js";
const getNotificationsQuery = query(async (unreadOnly: boolean) => {
const response = await fetch(
`/api/notifications?unreadOnly=${unreadOnly}`
);
return response.json();
}, "notifications");
function Notifications() {
const [unreadOnly, setUnreadOnly] = createSignal(false);
const notifications = createAsyncStore(() =>
getNotificationsQuery(unreadOnly())
);
return (
<div>
<button onClick={() => setUnreadOnly(!unreadOnly())}>
Toggle unread
</button>
<ul>
<For each={notifications()}>
{(notification) => (
<li>
<div>{notification.message}</div>
<div>{notification.user.name}</div>
</li>
)}
</For>
</ul>
</div>
);
}
const data = createAsyncStore(
() => fetchData(),
{
reconcile: {
key: "id", // Match items by id
merge: false, // Replace non-matching items
},
}
);
Benefits:
createAsync:
createAsyncStore:
revalidate manually refreshes cached queries. Use for polling, manual refetch, or after mutations.
import { query, createAsync, revalidate } from "@solidjs/router";
import { For } from "solid-js";
const getPosts = query(async () => {
return await fetch("https://api.com/posts").then((r) => r.json());
}, "posts");
function Posts() {
const posts = createAsync(() => getPosts());
function refetchPosts() {
revalidate(getPosts.key);
}
return (
<div>
<button onClick={refetchPosts}>Refetch posts</button>
<ul>
<For each={posts()}>
{(post) => <li>{post.title}</li>}
</For>
</ul>
</div>
);
}
// Revalidate all queries with base key
revalidate(getPosts.key);
// Revalidate specific query
revalidate(getPosts.keyFor("123"));
// Revalidate multiple keys
revalidate([getPosts.key, getUsers.key]);
// Revalidate everything
revalidate(undefined);
// Delete cache and refetch
revalidate(getPosts.key, { force: true });
// Default behavior (force: true)
revalidate(getPosts.key);
Parameters:
key: Query key or array of keysforce: Delete cache before refetch (default: true)const getUserQuery = query(
async (id: string, options = {}) => {
const params = new URLSearchParams({
summary: String(options.summary || false),
});
const response = await fetch(`/api/users/${id}?${params}`);
return response.json();
},
"usersById"
);
// Different arguments = different cache keys
getUserQuery("123"); // Cache key: usersById[123,{}]
getUserQuery("123", { summary: true }); // Cache key: usersById[123,{"summary":true}]
import { lazy, Route } from "@solidjs/router";
import { getUserQuery } from "./queries";
const User = lazy(() => import("./pages/users/[id].js"));
// Preload function
function preloadUser({ params, location }) {
void getUserQuery(params.id);
}
// Pass in route definition
<Route path="/users/:id" component={User} preload={preloadUser} />;
import { ErrorBoundary, Suspense } from "solid-js";
import { createAsync, query } from "@solidjs/router";
const getUserQuery = query(async (id: string) => {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error("Failed to fetch user");
}
return response.json();
}, "user");
function UserProfile() {
const user = createAsync(() => getUserQuery("123"));
return (
<ErrorBoundary
fallback={(err) => (
<div>
<p>Error: {err.message}</p>
<button onClick={() => revalidate(getUserQuery.keyFor("123"))}>
Retry
</button>
</div>
)}
>
<Suspense fallback={<p>Loading...</p>}>
<div>{user()!.name}</div>
</Suspense>
</ErrorBoundary>
);
}
import { onMount, onCleanup } from "solid-js";
import { createAsync, query, revalidate } from "@solidjs/router";
const getStatusQuery = query(async () => {
return fetch("/api/status").then((r) => r.json());
}, "status");
function Status() {
const status = createAsync(() => getStatusQuery());
onMount(() => {
const interval = setInterval(() => {
revalidate(getStatusQuery.key);
}, 5000); // Poll every 5 seconds
onCleanup(() => clearInterval(interval));
});
return <div>Status: {status()?.value}</div>;
}
import { action, revalidate } from "@solidjs/router";
import { getUserQuery } from "./queries";
const updateUserAction = action(async (userId: string, data: FormData) => {
await fetch(`/api/users/${userId}`, {
method: "PUT",
body: data,
});
// Revalidate after mutation
revalidate(getUserQuery.keyFor(userId));
}, "updateUser");
const getUserQuery = query(async (id: string) => {
return fetch(`/api/users/${id}`).then((r) => r.json());
}, "user");
const getUserPostsQuery = query(async (id: string) => {
return fetch(`/api/users/${id}/posts`).then((r) => r.json());
}, "userPosts");
function UserProfile() {
const [userId] = useParams();
const user = createAsync(() => getUserQuery(userId));
const posts = createAsync(() => getUserPostsQuery(userId));
return (
<Suspense fallback={<p>Loading...</p>}>
<div>
<h1>{user()!.name}</h1>
<For each={posts()}>
{(post) => <article>{post.title}</article>}
</For>
</div>
</Suspense>
);
}
Note: cache is deprecated. Use query instead.
query advantages:
Always use query for API calls:
Use createAsync for reactive data:
Use createAsyncStore for complex data:
Revalidate after mutations:
Handle errors and loading:
Use meaningful query names: