Fragment Colocation
Fragment colocation is a pattern where GraphQL fragments are defined alongside the components that use them. soda-gql provides powerful tools for this pattern with type-safe data projection.
How soda-gql Colocation Differs from GraphQL
Standard GraphQL colocation (e.g., with Relay or Apollo) uses fragment spreads:
# UserCard.graphql
fragment UserCardFragment on User {
id
name
avatarUrl
}
# Parent query
query UserPage($id: ID!) {
user(id: $id) {
...UserCardFragment
...PostListFragment
}
}
soda-gql takes a different approach:
| Aspect | Traditional GraphQL | soda-gql |
|---|
| Composition | Fragment spread ...Name | $colocate({ label: fragment.spread() }) |
| Labeling | Implicit by fragment name | Explicit labels for each slice |
| Data Extraction | Manual traversal | createExecutionResultParser with projections |
| Error Handling | Manual per-fragment | Built-in SlicedExecutionResult with Success/Error/Empty states |
| Type Safety | Requires codegen | Full inference with Projection types |
TIP
The $colocate helper with explicit labels enables soda-gql to route data and errors to the correct fragment handlers automatically.
The Colocation Workflow
Step 1: Define Component Fragment with Projection
Each component defines its fragment and attaches a projection for data extraction:
// UserCard.tsx
import { gql } from "@/graphql-system";
import { createProjectionAttachment } from "@soda-gql/colocation-tools";
export const userCardFragment = gql
.default(({ fragment }) =>
fragment("UserCard", "Query")`($userId: ID!) {
user(id: $userId) {
id
name
avatarUrl
}
}`(),
)
.attach(
createProjectionAttachment({
paths: ["$.user"],
handle: (result) => {
if (result.isError()) {
return { error: result.error, user: null };
}
if (result.isEmpty()) {
return { error: null, user: null };
}
const [user] = result.unwrap();
return { error: null, user };
},
}),
);
// Component using the fragment data
export function UserCard({
data,
}: {
data: ReturnType<typeof userCardFragment.projection.projector>;
}) {
if (data.error) return <ErrorDisplay error={data.error} />;
if (!data.user) return <Loading />;
return <div>{data.user.name}</div>;
}
Step 2: Compose Fragments in Parent Operation
Use $colocate to combine multiple fragments with explicit labels:
// UserPage.tsx
import { gql } from "@/graphql-system";
import { userCardFragment } from "./UserCard";
import { postListFragment } from "./PostList";
export const userPageQuery = gql.default(({ query, $colocate }) =>
query("UserPage")({
variables: `($userId: ID!)`,
fields: ({ $ }) => $colocate({
userCard: userCardFragment.spread({ userId: $.userId }),
postList: postListFragment.spread({ userId: $.userId }),
}),
})({}),
);
Step 3: Create Result Parser
Create a parser that routes data to each fragment's projection:
import { createExecutionResultParser } from "@soda-gql/colocation-tools";
const parseUserPageResult = createExecutionResultParser({
userCard: userCardFragment,
postList: postListFragment,
});
Step 4: Execute and Distribute Data
Execute the query and distribute results to components:
// In your page component
async function UserPage({ userId }: { userId: string }) {
const response = await graphqlClient({
document: userPageQuery.document,
variables: { userId },
});
const { userCard, postList } = parseUserPageResult(response);
return (
<div>
<UserCard data={userCard} />
<PostList data={postList} />
</div>
);
}
Projections
Projections define how to extract and transform data from execution results.
createProjectionAttachment
Attach a projection directly to a fragment:
import { createProjectionAttachment } from "@soda-gql/colocation-tools";
const fragment = gql
.default(({ fragment }) =>
fragment("UserData", "Query")`{
user(id: "1") {
id
name
}
}`(),
)
.attach(
createProjectionAttachment({
paths: ["$.user"],
handle: (result) => {
// Transform the sliced result (receives tuple of values for each path)
if (result.isError()) return { error: result.error };
if (result.isEmpty()) return { data: null };
const [user] = result.unwrap();
return { data: user };
},
}),
);
Projection Paths
Paths specify which parts of the result to extract:
paths: ["$.user"] // Extract user field
paths: ["$.user.posts"] // Extract nested field
paths: ["$.user", "$.meta"] // Extract multiple fields
Path format:
- Always start with
$.
- Use dot notation for nested fields
- The first path segment maps to the
$colocate label
SlicedExecutionResult
The handle function receives a SlicedExecutionResult, which can be one of three states:
Success
Data was extracted successfully:
handle: (result) => {
if (result.isSuccess()) {
const data = result.unwrap(); // Get typed data
return { data };
}
// ...
}
Error
An error occurred (GraphQL error, network error, or parse error):
handle: (result) => {
if (result.isError()) {
const error = result.error; // NormalizedError
return { error, data: null };
}
// ...
}
Empty
No data or error (null result):
handle: (result) => {
if (result.isEmpty()) {
return { data: null };
}
// ...
}
Safe Unwrapping
Use safeUnwrap for convenient error handling:
handle: (result) => {
const { data, error } = result.safeUnwrap((user) => ({
formatted: user.name.toUpperCase(),
}));
return { data, error };
}
Error Routing
The createExecutionResultParser automatically routes GraphQL errors to the correct fragments based on the error's path:
// If the GraphQL response contains:
{
"data": { "userCard_user": null },
"errors": [{
"message": "User not found",
"path": ["userCard_user"]
}]
}
// The error is routed to the userCard projection:
const { userCard } = parseResult(response);
userCard.error; // Contains the "User not found" error
Complete Example
// fragments/UserCard.ts
import { gql } from "@/graphql-system";
import { createProjectionAttachment } from "@soda-gql/colocation-tools";
export const userCardFragment = gql
.default(({ fragment }) =>
fragment("UserCard", "Query")`($id: ID!) {
user(id: $id) {
id
name
email
}
}`(),
)
.attach(
createProjectionAttachment({
paths: ["$.user"],
handle: (result) => result.safeUnwrap(([user]) => user),
}),
);
// fragments/PostList.ts
export const postListFragment = gql
.default(({ fragment }) =>
fragment("PostList", "Query")`($userId: ID!, $limit: Int) {
user(id: $userId) {
posts(limit: $limit) {
id
title
}
}
}`(),
)
.attach(
createProjectionAttachment({
paths: ["$.user.posts"],
handle: (result) => result.safeUnwrap(([posts]) => posts ?? []),
}),
);
// pages/UserPage.ts
import { createExecutionResultParser } from "@soda-gql/colocation-tools";
import { userCardFragment } from "./fragments/UserCard";
import { postListFragment } from "./fragments/PostList";
export const userPageQuery = gql.default(({ query, $colocate }) =>
query("UserPage")({
variables: `($userId: ID!)`,
fields: ({ $ }) => $colocate({
userCard: userCardFragment.spread({ id: $.userId }),
postList: postListFragment.spread({ userId: $.userId, limit: 10 }),
}),
})({}),
);
export const parseUserPageResult = createExecutionResultParser({
userCard: userCardFragment,
postList: postListFragment,
});
Single Fragment Operations
For simple operations like mutations with a single fragment spread, you can use
createDirectParser instead of $colocate + createExecutionResultParser:
import { createProjectionAttachment, createDirectParser } from "@soda-gql/colocation-tools";
// Define mutation fragment with projection
const createProductFragment = gql
.default(({ fragment }) =>
fragment("CreateProduct", "Mutation")`($input: ProductInput!) {
insert_products_one(object: $input) {
id
name
}
}`(),
)
.attach(
createProjectionAttachment({
paths: ["$.insert_products_one"],
handle: (result) => result.safeUnwrap(([product]) => product),
}),
);
// Create operation (no $colocate needed)
const createProductMutation = gql.default(({ mutation }) =>
mutation("CreateProduct")({
variables: `($input: ProductInput!)`,
fields: ({ $ }) => createProductFragment.spread({ input: $.input }),
})({}),
);
// Use createDirectParser (returns projected value directly)
const parseCreateProduct = createDirectParser(createProductFragment);
// Usage
const response = await client.execute(createProductMutation);
const { data, error } = parseCreateProduct(response);
This is simpler than $colocate when you only have one fragment to parse.
Next Steps