Documentation Index
Fetch the complete documentation index at: https://kubo-47e69177.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Context7 Recipes
1) createUser action with Zod input/output
import { defineAction } from "@zapaction/core";
import { z } from "zod";
type User = { id: string; name: string; email: string };
const users = new Map<string, User>();
export const createUser = defineAction({
input: z.object({
name: z.string().min(1),
email: z.string().email(),
}),
output: z.object({
id: z.string(),
email: z.string().email(),
}),
tags: ["users"],
handler: async ({ input }) => {
const duplicate = Array.from(users.values()).some((user) => user.email === input.email);
if (duplicate) {
throw new Error("Email already in use");
}
const id = crypto.randomUUID();
users.set(id, { id, name: input.name, email: input.email });
return { id, email: input.email };
},
});
2) Global context with setActionContext
import { defineAction, setActionContext } from "@zapaction/core";
import { z } from "zod";
type AppContext = {
userId: string;
isAdmin: boolean;
};
setActionContext<AppContext>(async () => {
const session = { user: { id: "u_123", role: "admin" as const } };
return {
userId: session.user.id,
isAdmin: session.user.role === "admin",
};
});
export const whoAmI = defineAction<{}, { userId: string; isAdmin: boolean }, AppContext>({
input: z.object({}),
output: z.object({
userId: z.string(),
isAdmin: z.boolean(),
}),
handler: async ({ ctx }) => ({
userId: ctx.userId,
isAdmin: ctx.isAdmin,
}),
});
3) React button + useAction (submitForm)
// app/actions.ts
import { defineAction } from "@zapaction/core";
import { z } from "zod";
export const submitForm = defineAction({
input: z.object({ message: z.string().min(1) }),
output: z.object({ success: z.boolean() }),
handler: async ({ input }) => {
console.log("message:", input.message);
return { success: true };
},
});
// components/submit-form-button.tsx
"use client";
import { useState } from "react";
import { useAction } from "@zapaction/react";
import { submitForm } from "../app/actions";
export function SubmitFormButton() {
const [message, setMessage] = useState("");
const action = useAction(submitForm);
const handleClick = async () => {
const result = await action.execute({ message });
if (result.success) {
setMessage("");
}
};
return (
<div>
<input value={message} onChange={(e) => setMessage(e.target.value)} />
<button onClick={handleClick} disabled={action.isPending}>
{action.isPending ? "Sending..." : "Send"}
</button>
</div>
);
}
4) getPosts + optional limit with useActionQuery
// app/actions.ts
import { defineAction } from "@zapaction/core";
import { z } from "zod";
const postSchema = z.object({
id: z.string(),
title: z.string(),
});
const posts = [
{ id: "p1", title: "First post" },
{ id: "p2", title: "Second post" },
{ id: "p3", title: "Third post" },
];
export const getPosts = defineAction({
input: z.object({
limit: z.number().int().positive().optional(),
}),
output: z.array(postSchema),
handler: async ({ input }) => {
return typeof input.limit === "number" ? posts.slice(0, input.limit) : posts;
},
});
// components/posts-list.tsx
"use client";
import { createFeatureKeys } from "@zapaction/core";
import { useActionQuery } from "@zapaction/query";
import { getPosts } from "../app/actions";
const postKeys = createFeatureKeys("posts", {
list: (limit?: number) => ["list", limit ?? "all"],
});
export function PostsList({ limit }: { limit?: number }) {
const query = useActionQuery(getPosts, {
input: { limit },
queryKey: postKeys.list(limit),
readPolicy: "read-only",
});
if (query.isPending) return <p>Loading...</p>;
if (query.isError) return <p>Failed to load posts</p>;
return (
<ul>
{query.data?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
5) updateProfile + useActionMutation
// app/actions.ts
import { defineAction } from "@zapaction/core";
import { z } from "zod";
const emailsByUserId = new Map<string, string>([["u_1", "old@mail.com"]]);
export const updateProfile = defineAction({
input: z.object({
userId: z.string(),
newEmail: z.string().email(),
}),
output: z.object({
email: z.string().email(),
}),
tags: ["users"],
handler: async ({ input }) => {
emailsByUserId.set(input.userId, input.newEmail);
return { email: input.newEmail };
},
});
// components/profile-email-form.tsx
"use client";
import { useState } from "react";
import { useActionMutation } from "@zapaction/query";
import { updateProfile } from "../app/actions";
export function ProfileEmailForm({ userId }: { userId: string }) {
const [newEmail, setNewEmail] = useState("");
const mutation = useActionMutation(updateProfile);
const onSave = async () => {
const result = await mutation.mutateAsync({ userId, newEmail });
console.log("Updated email:", result.email);
};
return (
<div>
<input value={newEmail} onChange={(e) => setNewEmail(e.target.value)} />
<button onClick={onSave} disabled={mutation.isPending}>
Save
</button>
</div>
);
}
6) Revalidate all posts queries after create
// app/query-model.ts
import { createFeatureKeys, createFeatureTags } from "@zapaction/core";
export const postKeys = createFeatureKeys("posts", {
list: () => ["list"],
});
export const postTags = createFeatureTags("posts", {
list: () => ["list"],
});
// app/actions.ts
import { defineAction } from "@zapaction/core";
import { z } from "zod";
import { postTags } from "./query-model";
export const createBlogPost = defineAction({
input: z.object({ title: z.string().min(1) }),
output: z.object({ id: z.string(), title: z.string() }),
tags: [postTags.all()],
handler: async ({ input }) => ({ id: crypto.randomUUID(), title: input.title }),
});
// app/providers.tsx
import { setTagRegistry } from "@zapaction/query";
import { postKeys, postTags } from "./query-model";
setTagRegistry({
[postTags.all()]: [postKeys.list()],
});
useActionMutation(createBlogPost) invalidates mapped queries on success.
7) Invalidate all users caches after delete
// app/query-model.ts
import { createFeatureTags } from "@zapaction/core";
export const userTags = createFeatureTags("users", {
list: () => ["list"],
detail: (userId: string) => ["detail", userId],
});
// app/providers.tsx
import { setTagRegistry } from "@zapaction/query";
import { userTags } from "./query-model";
setTagRegistry({
[userTags.all()]: [["users"]],
});
// app/actions.ts
import { defineAction } from "@zapaction/core";
import { z } from "zod";
import { userTags } from "./query-model";
export const deleteUser = defineAction({
input: z.object({ userId: z.string() }),
output: z.object({ deleted: z.literal(true) }),
tags: [userTags.all()],
handler: async () => ({ deleted: true }),
});
["users"] is a prefix query key and invalidates all ["users", ...] entries.
8) Typed key for getProductDetails with createQueryKeys
import { createQueryKeys, defineAction } from "@zapaction/core";
import { useActionQuery } from "@zapaction/query";
import { z } from "zod";
export const getProductDetails = defineAction({
input: z.object({ productId: z.string() }),
output: z.object({ id: z.string(), title: z.string(), price: z.number() }),
handler: async ({ input }) => ({
id: input.productId,
title: "Demo product",
price: 42,
}),
});
export const queryKeys = createQueryKeys({
getProductDetails: (productId: string) => ["products", "detail", productId] as const,
});
const productId = "p_123";
const productQuery = useActionQuery(getProductDetails, {
input: { productId },
queryKey: queryKeys.getProductDetails(productId),
readPolicy: "read-only",
});
9) Admin-only deleteSensitiveData
import { defineAction, setActionContext } from "@zapaction/core";
import { z } from "zod";
type AppContext = { userId: string; isAdmin: boolean };
setActionContext<AppContext>(async () => ({
userId: "u_123",
isAdmin: true,
}));
export const deleteSensitiveData = defineAction<
{ recordId: string },
{ deleted: true },
AppContext
>({
input: z.object({ recordId: z.string() }),
output: z.object({ deleted: z.literal(true) }),
beforeAction: ({ ctx }) => {
if (!ctx.isAdmin) {
throw new Error("Forbidden");
}
},
handler: async () => ({ deleted: true }),
});
10) Invalidate postDetails (entity) + recentComments (global)
For entity-specific invalidation, pass runtime tags with invalidateWithTags.
// app/query-model.ts
import { createFeatureKeys, createFeatureTags } from "@zapaction/core";
export const postKeys = createFeatureKeys("posts", {
detail: (postId: string) => ["detail", postId],
});
export const commentKeys = createFeatureKeys("comments", {
recent: () => ["recent"],
});
export const postTags = createFeatureTags("posts", {
detail: (postId: string) => ["detail", postId],
});
export const commentTags = createFeatureTags("comments", {
recent: () => ["recent"],
});
// app/providers.tsx
import { setTagRegistry } from "@zapaction/query";
import { commentKeys, commentTags, postKeys, postTags } from "./query-model";
const postId = "post_42";
setTagRegistry({
[postTags.detail(postId)]: [postKeys.detail(postId)],
[commentTags.recent()]: [commentKeys.recent()],
});
// app/actions.ts
import { defineAction } from "@zapaction/core";
import { z } from "zod";
export const createComment = defineAction({
input: z.object({
postId: z.string(),
message: z.string().min(1),
}),
output: z.object({
id: z.string(),
postId: z.string(),
message: z.string(),
}),
handler: async ({ input }) => ({
id: crypto.randomUUID(),
postId: input.postId,
message: input.message,
}),
});
// components/comment-form.tsx
"use client";
import { useActionMutation } from "@zapaction/query";
import { createComment } from "../app/actions";
import { commentTags, postTags } from "../app/query-model";
export function CommentForm({ postId }: { postId: string }) {
const mutation = useActionMutation(createComment, {
invalidateWithTags: [postTags.detail(postId), commentTags.recent()],
});
return (
<button onClick={() => mutation.mutate({ postId, message: "Nice post!" })}>
Add comment
</button>
);
}