docs/basics/solid-start.md
This guide covers how to use @urql/solid-start with SolidStart applications. The @urql/solid-start package integrates urql with SolidStart's native data fetching primitives like query(), action(), createAsync(), and useAction().
Note: This guide is for SolidStart applications with SSR. If you're building a client-side only SolidJS app, see the Solid guide instead. See the comparison section below for key differences between the packages.
Installing @urql/solid-start requires both the package and its peer dependencies:
yarn add @urql/solid-start @urql/solid @urql/core graphql
# or
npm install --save @urql/solid-start @urql/solid @urql/core graphql
# or
pnpm add @urql/solid-start @urql/solid @urql/core graphql
The @urql/solid-start package depends on @urql/solid for shared utilities and re-exports some primitives that work identically on both client and server.
ClientThe @urql/solid-start package exports a Client class from @urql/core. This central Client manages all of our GraphQL requests and results.
import { createClient, cacheExchange, fetchExchange } from '@urql/solid-start';
const client = createClient({
url: 'http://localhost:3000/graphql',
exchanges: [cacheExchange, fetchExchange],
});
At the bare minimum we'll need to pass an API's url and exchanges when we create a Client.
For server-side requests, you'll often want to customize fetchOptions to include headers like cookies or authorization tokens:
import { getRequestEvent } from 'solid-js/web';
const client = createClient({
url: 'http://localhost:3000/graphql',
exchanges: [cacheExchange, fetchExchange],
fetchOptions: () => {
const event = getRequestEvent();
return {
headers: {
cookie: event?.request.headers.get('cookie') || '',
},
};
},
});
ClientTo make use of the Client in SolidStart we will provide it via Solid's Context API using the Provider export. The Provider also needs the query and action functions from @solidjs/router:
// src/root.tsx or src/app.tsx
import { Router, action, query } from '@solidjs/router';
import { FileRoutes } from '@solidjs/start/router';
import { Suspense } from 'solid-js';
import { createClient, Provider, cacheExchange, fetchExchange } from '@urql/solid-start';
const client = createClient({
url: 'http://localhost:3000/graphql',
exchanges: [cacheExchange, fetchExchange],
});
export default function App() {
return (
<Router
root={props => (
<Provider value={{ client, query, action }}>
<Suspense>{props.children}</Suspense>
</Provider>
)}
>
<FileRoutes />
</Router>
);
}
Now every route and component inside the Provider can use GraphQL queries and mutations that will be sent to our API. The query and action functions are provided in context so that createQuery and createMutation can access them automatically.
The @urql/solid-start package offers a createQuery primitive that integrates with SolidStart's query() and createAsync() primitives for optimal server-side rendering and streaming.
For the following examples, we'll imagine that we're querying data from a GraphQL API that contains todo items.
// src/routes/todos.tsx
import { Suspense, For, Show } from 'solid-js';
import { createAsync } from '@solidjs/router';
import { gql } from '@urql/core';
import { createQuery } from '@urql/solid-start';
const TodosQuery = gql`
query {
todos {
id
title
}
}
`;
export default function Todos() {
const queryTodos = createQuery(TodosQuery, 'todos-list');
const result = createAsync(() => queryTodos());
return (
<Suspense fallback={<p>Loading...</p>}>
<Show when={result()?.data}>
<ul>
<For each={result()!.data.todos}>
{(todo) => <li>{todo.title}</li>}
</For>
</ul>
</Show>
</Suspense>
);
}
The createQuery primitive integrates with SolidStart's data fetching system:
query() function to execute URQL queries with proper router contextquery function is automatically retrieved from the URQL context (no manual injection needed)createAsync() to get the reactive resultcreateQuery must be called inside a component where it has access to the contextThe query automatically executes on both the server (during SSR) and the client, with SolidStart handling serialization and hydration.
Typically we'll also need to pass variables to our queries. Pass variables as an option in the fourth parameter:
// src/routes/todos/[page].tsx
import { Suspense, For, Show } from 'solid-js';
import { useParams, createAsync } from '@solidjs/router';
import { gql } from '@urql/core';
import { createQuery } from '@urql/solid-start';
const TodosListQuery = gql`
query ($from: Int!, $limit: Int!) {
todos(from: $from, limit: $limit) {
id
title
}
}
`;
export default function TodosPage() {
const params = useParams();
const queryTodos = createQuery(TodosListQuery, 'todos-paginated', {
variables: {
from: parseInt(params.page) * 10,
limit: 10,
},
});
const result = createAsync(() => queryTodos());
return (
<Suspense fallback={<p>Loading...</p>}>
<Show when={result()?.data}>
<ul>
<For each={result()!.data.todos}>
{(todo) => <li>{todo.title}</li>}
</For>
</ul>
</Show>
</Suspense>
);
}
For dynamic variables that change based on reactive values, you'll need to recreate the query function when dependencies change.
The requestPolicy option determines how results are retrieved from the cache:
const queryTodos = createQuery(TodosQuery, 'todos-list', {
requestPolicy: 'cache-and-network',
});
const result = createAsync(() => queryTodos());
Available policies:
cache-first (default): Prefer cached results, fall back to networkcache-only: Only use cached results, never send network requestsnetwork-only: Always send a network request, ignore cachecache-and-network: Return cached results immediately, then fetch from networkLearn more about request policies on the "Document Caching" page.
There are two approaches to revalidating data in SolidStart with urql:
Both approaches work well, and you can choose based on your needs. urql's invalidation is more granular and works at the query level, while SolidStart's revalidation works at the route level.
You can manually revalidate queries using urql's cache invalidation with the keyFor helper. This invalidates specific queries in urql's cache and triggers automatic refetches:
// src/routes/todos.tsx
import { Suspense, For, Show } from 'solid-js';
import { createAsync } from '@solidjs/router';
import { gql, keyFor } from '@urql/core';
import { createQuery, useClient } from '@urql/solid-start';
const TodosQuery = gql`
query {
todos {
id
title
}
}
`;
export default function Todos() {
const client = useClient();
const queryTodos = createQuery(TodosQuery, 'todos-list');
const result = createAsync(() => queryTodos());
const handleRefresh = () => {
// Invalidate the todos query using keyFor
const key = keyFor(TodosQuery);
client.reexecuteOperation(client.createRequestOperation('query', {
key,
query: TodosQuery
}));
};
return (
<div>
<button onClick={handleRefresh}>Refresh Todos</button>
<Suspense fallback={<p>Loading...</p>}>
<Show when={result()?.data}>
<ul>
<For each={result()!.data.todos}>
{(todo) => <li>{todo.title}</li>}
</For>
</ul>
</Show>
</Suspense>
</div>
);
}
Alternatively, you can use SolidStart's built-in revalidate function to reload route data. This is useful when you want to refresh all queries on a specific route:
// src/routes/todos.tsx
import { Suspense, For, Show } from 'solid-js';
import { createAsync, revalidate } from '@solidjs/router';
import { gql } from '@urql/core';
import { createQuery } from '@urql/solid-start';
const TodosQuery = gql`
query {
todos {
id
title
}
}
`;
export default function Todos() {
const queryTodos = createQuery(TodosQuery, 'todos-list');
const result = createAsync(() => queryTodos());
const handleRefresh = async () => {
// Revalidate the current route - refetches all queries on this page
await revalidate();
};
return (
<div>
<button onClick={handleRefresh}>Refresh Todos</button>
<Suspense fallback={<p>Loading...</p>}>
<Show when={result()?.data}>
<ul>
<For each={result()!.data.todos}>
{(todo) => <li>{todo.title}</li>}
</For>
</ul>
</Show>
</Suspense>
</div>
);
}
A common pattern is to revalidate after a mutation succeeds. You can choose either approach:
Using urql's cache invalidation:
// src/routes/todos/new.tsx
import { useNavigate } from '@solidjs/router';
import { gql, keyFor } from '@urql/core';
import { createMutation, useClient } from '@urql/solid-start';
const TodosQuery = gql`
query {
todos {
id
title
}
}
`;
const CreateTodo = gql`
mutation ($title: String!) {
createTodo(title: $title) {
id
title
}
}
`;
export default function NewTodo() {
const navigate = useNavigate();
const client = useClient();
const [state, createTodo] = createMutation(CreateTodo);
const handleSubmit = async (e: Event) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const title = formData.get('title') as string;
const result = await createTodo({ title });
if (!result.error) {
// Invalidate todos query using keyFor
const key = keyFor(TodosQuery);
client.reexecuteOperation(client.createRequestOperation('query', {
key,
query: TodosQuery
}));
navigate('/todos');
}
};
return (
<form onSubmit={handleSubmit}>
<input name="title" type="text" required />
<button type="submit" disabled={state.fetching}>
{state.fetching ? 'Creating...' : 'Create Todo'}
</button>
</form>
);
}
Using SolidStart's revalidation:
// src/routes/todos/new.tsx
import { useNavigate } from '@solidjs/router';
import { gql } from '@urql/core';
import { createMutation } from '@urql/solid-start';
import { revalidate } from '@solidjs/router';
const CreateTodo = gql`
mutation ($title: String!) {
createTodo(title: $title) {
id
title
}
}
`;
export default function NewTodo() {
const navigate = useNavigate();
const [state, createTodo] = createMutation(CreateTodo);
const handleSubmit = async (e: Event) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const title = formData.get('title') as string;
const result = await createTodo({ title });
if (!result.error) {
// Revalidate the /todos route to refetch all its queries
await revalidate('/todos');
navigate('/todos');
}
};
return (
<form onSubmit={handleSubmit}>
<input name="title" type="text" required />
<button type="submit" disabled={state.fetching}>
{state.fetching ? 'Creating...' : 'Create Todo'}
</button>
</form>
);
}
When using SolidStart actions, you can configure automatic revalidation by returning the appropriate response:
import { action, revalidate } from '@solidjs/router';
import { gql } from '@urql/core';
const createTodoAction = action(async (formData: FormData) => {
const title = formData.get('title') as string;
// Perform mutation
const result = await client.mutation(CreateTodo, { title }).toPromise();
if (!result.error) {
// Revalidate multiple routes if needed
await revalidate(['/todos', '/']);
}
return result;
});
Use urql's keyFor and reexecuteOperation when:
Use SolidStart's revalidate when:
Both approaches are valid and can even be used together depending on your application's needs.
Context options can be passed to customize the query behavior:
const queryTodos = createQuery(TodosQuery, 'todos-list', {
context: {
requestPolicy: 'cache-and-network',
fetchOptions: {
headers: {
'X-Custom-Header': 'value',
},
},
},
});
const result = createAsync(() => queryTodos());
You can find a list of all Context options in the API docs.
The @urql/solid-start package offers a createMutation primitive that integrates with SolidStart's action() and useAction() primitives.
Mutations in SolidStart are executed using actions. Here's an example of updating a todo item:
// src/routes/todos/[id]/edit.tsx
import { gql } from '@urql/core';
import { createMutation } from '@urql/solid-start';
import { useParams, useNavigate } from '@solidjs/router';
import { Show } from 'solid-js';
const UpdateTodo = gql`
mutation ($id: ID!, $title: String!) {
updateTodo(id: $id, title: $title) {
id
title
}
}
`;
export default function EditTodo() {
const params = useParams();
const navigate = useNavigate();
const [state, updateTodo] = createMutation(UpdateTodo);
const handleSubmit = async (e: Event) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const title = formData.get('title') as string;
const result = await updateTodo({
id: params.id,
title,
});
if (!result.error) {
navigate(`/todos/${params.id}`);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="title" type="text" required />
<button type="submit" disabled={state.fetching}>
{state.fetching ? 'Saving...' : 'Save'}
</button>
<Show when={state.error}>
<p style={{ color: 'red' }}>Error: {state.error.message}</p>
</Show>
</form>
);
}
The createMutation primitive returns a tuple:
fetching, error, and dataYou can optionally provide a custom key parameter to control how mutations are cached by SolidStart's router:
const [state, updateTodo] = createMutation(UpdateTodo, 'update-todo-mutation');
SolidStart actions work with and without JavaScript enabled. Here's how to set up a mutation that works progressively:
import { action, redirect } from '@solidjs/router';
import { gql } from '@urql/core';
import { createMutation } from '@urql/solid-start';
const CreateTodo = gql`
mutation ($title: String!) {
createTodo(title: $title) {
id
title
}
}
`;
export default function NewTodo() {
const [state, createTodo] = createMutation(CreateTodo);
const handleSubmit = async (formData: FormData) => {
const title = formData.get('title') as string;
const result = await createTodo({ title });
if (!result.error) {
return redirect('/todos');
}
};
return (
<form action={handleSubmit} method="post">
<input name="title" type="text" required />
<button type="submit" disabled={state.fetching}>
{state.fetching ? 'Creating...' : 'Create Todo'}
</button>
<Show when={state.error}>
<p style={{ color: 'red' }}>Error: {state.error.message}</p>
</Show>
</form>
);
}
The mutation state is reactive and updates automatically as the mutation progresses:
const [state, updateTodo] = createMutation(UpdateTodo);
createEffect(() => {
if (state.data) {
console.log('Mutation succeeded:', state.data);
}
if (state.error) {
console.error('Mutation failed:', state.error);
}
if (state.fetching) {
console.log('Mutation in progress...');
}
});
The execute function also returns a promise that resolves to the result:
const [state, updateTodo] = createMutation(UpdateTodo);
const handleUpdate = async () => {
const result = await updateTodo({ id: '1', title: 'Updated' });
if (result.error) {
console.error('Oh no!', result.error);
} else {
console.log('Success!', result.data);
}
};
Mutation promises never reject. Instead, check the error field on the result:
const [state, updateTodo] = createMutation(UpdateTodo);
const handleUpdate = async () => {
const result = await updateTodo({ id: '1', title: 'Updated' });
if (result.error) {
// CombinedError with network or GraphQL errors
console.error('Mutation failed:', result.error);
// Check for specific error types
if (result.error.networkError) {
console.error('Network error:', result.error.networkError);
}
if (result.error.graphQLErrors.length > 0) {
console.error('GraphQL errors:', result.error.graphQLErrors);
}
}
};
Read more about error handling on the "Errors" page.
For GraphQL subscriptions, @urql/solid-start provides a createSubscription primitive that uses the same SolidStart Provider context as createQuery and createMutation:
import { gql } from '@urql/core';
import { createSubscription } from '@urql/solid-start';
import { createSignal, For } from 'solid-js';
const NewTodos = gql`
subscription {
newTodos {
id
title
}
}
`;
export default function TodoSubscription() {
const [todos, setTodos] = createSignal([]);
const handleSubscription = (previousData, newData) => {
setTodos(current => [...current, newData.newTodos]);
return newData;
};
const [result] = createSubscription(
{
query: NewTodos,
},
handleSubscription
);
return (
<div>
<h2>Live Updates</h2>
<ul>
<For each={todos()}>{todo => <li>{todo.title}</li>}</For>
</ul>
</div>
);
}
Note that GraphQL subscriptions typically require WebSocket support. You'll need to configure your client with a subscription exchange like subscriptionExchange from @urql/core.
SolidStart automatically handles server-side rendering and hydration. The createQuery primitive works seamlessly on both server and client:
When using createQuery in SolidStart:
For authenticated requests, forward cookies and headers from the server request:
import { getRequestEvent } from 'solid-js/web';
import { createClient, cacheExchange, fetchExchange } from '@urql/solid-start';
const client = createClient({
url: 'http://localhost:3000/graphql',
exchanges: [cacheExchange, fetchExchange],
fetchOptions: () => {
const event = getRequestEvent();
const headers: Record<string, string> = {};
// Forward cookies for authenticated requests
if (event) {
const cookie = event.request.headers.get('cookie');
if (cookie) {
headers.cookie = cookie;
}
}
return { headers };
},
});
| Use Case | Package | Why |
|---|---|---|
| Client-side SPA | @urql/solid | Optimized for client-only apps, uses SolidJS reactivity patterns |
| SolidStart SSR App | @urql/solid-start | Integrates with SolidStart's routing, SSR, and action system |
@urql/solid (Client-side):
import { createQuery } from '@urql/solid';
const [result] = createQuery({ query: TodosQuery });
// Returns: [Accessor<OperationResult>, Accessor<ReExecute>]
@urql/solid-start (SSR):
import { createQuery } from '@urql/solid-start';
import { createAsync } from '@solidjs/router';
const queryTodos = createQuery(TodosQuery, 'todos');
const todos = createAsync(() => queryTodos());
// Returns: Accessor<OperationResult | undefined>
// Works with SSR and SolidStart's caching
@urql/solid (Client-side):
import { createMutation } from '@urql/solid';
const [result, executeMutation] = createMutation(AddTodoMutation);
await executeMutation({ title: 'New Todo' });
// Returns: [Accessor<OperationResult>, ExecuteMutation]
@urql/solid-start (SSR with Actions):
import { createMutation } from '@urql/solid-start';
import { useAction, useSubmission } from '@solidjs/router';
const addTodoAction = createMutation(AddTodoMutation, 'add-todo');
const addTodo = useAction(addTodoAction);
const submission = useSubmission(addTodoAction);
await addTodo({ title: 'New Todo' });
// Integrates with SolidStart's action system for progressive enhancement
If you're moving from a SolidJS SPA to SolidStart:
@urql/solid to @urql/solid-startcreateAsync()useAction() and useSubmission()For more details, see the Solid bindings documentation.
This concludes the introduction for using @urql/solid-start with SolidStart. For more information: