Skip to main content

Getting Started with Zustic Query

Set up Zustic Query in minutes and start building robust server state management. This guide walks through installing, configuring, and using the Query API in your React application.

Features Overview

Zustic Query provides everything needed for modern data fetching:

  • Queries & Mutations — Read and write operations with built-in state management
  • Intelligent Caching — Automatic cache management with configurable expiration
  • Manual Refetching — Force fresh data when needed
  • Middleware Pipeline — Intercept and transform requests/responses
  • Plugin System — Extend functionality with lifecycle hooks
  • Response Transformation — Normalize API responses to app formats

Installation

Install the Zustic package:

npm install zustic

Import the API factory from the query submodule:

import { createApi } from 'zustic/query'

Step 1: Define Your API Configuration

Create src/api.ts to centralize your server state management:

import { createApi } from "zustic/query";

interface User {
id: number;
name: string;
email: string;
}

export const api = createApi({
// Required: base query handler
baseQuery: async (params) => {
try {
const res = await fetch(
`https://jsonplaceholder.typicode.com${params.url}`,
{
method: params.method || "GET",
headers: params.headers,
body: params.body ? JSON.stringify(params.body) : undefined,
}
);

if (!res.ok) {
return { error: `HTTP ${res.status}` };
}

return { data: await res.json() };
} catch (error) {
return {
error:
error instanceof Error ? error.message : "Unknown error",
};
}
},

// Cache duration (default: 30 seconds)
cacheTimeout: 60 * 1000,

endpoints: (builder) => ({
// Query endpoint
getUsers: builder.query({
query: () => ({
url: "/users",
method: "GET",
}),
}),

// Query with param
getUser: builder.query({
query: (id: number) => ({
url: `/users/${id}`,
}),
}),

// Mutation endpoint
createUser: builder.mutation({
query: (user: Omit<User, "id">) => ({
url: "/users",
method: "POST",
body: user,
}),
}),
}),
});

export const {
useGetUsersQuery,
useGetUserQuery,
useCreateUserMutation,
} = api;

Step 2: Using Query Hooks

Query hooks automatically fetch data when components mount (unless explicitly skipped). Auto-generated hook names follow the pattern use{EndpointName}Query.

Fetching List Data

import { useGetUsersQuery } from "./api";

export function UsersList() {
const {
data,
isLoading,
isError,
isSuccess,
error,
reFetch,
} = useGetUsersQuery(undefined);

if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {String(error)}</div>;

return (
<div>
<button onClick={() => reFetch()}>
Refresh
</button>

<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}

Query Hook Return Values

Every query hook returns a state object with the following properties:

const {
data,
isLoading,
isError,
isSuccess,
error,
reFetch,
} = useGetUsersQuery(arg, options)
PropertyTypeDescription
dataTThe resolved response data
isLoadingbooleanTrue during the initial request
isErrorbooleanTrue if the request failed
isSuccessbooleanTrue if the request completed
errorstring?Error message if request failed
reFetch() => voidFunction to manually trigger fresh data

Conditional Queries

Use the skip option to prevent queries from executing until specific conditions are met.

Skip Until Data Available

export function UserProfile({ userId }: { userId?: number }) {
// Query only executes if userId is defined
const { data } = useGetUserQuery(userId, {
skip: !userId,
})

if (!userId) return <div>Select a user first</div>

return <div>{data?.name}</div>
}

Step 3: Using Mutation Hooks

Mutations represent write operations (POST, PUT, DELETE) that are manually triggered on demand.

Creating Resources

import { useCreateUserMutation } from "./api";

export function CreateUserForm() {
const [createUser, { isLoading, isError, error }] =
useCreateUserMutation();

const handleSubmit = async (
e: React.FormEvent<HTMLFormElement>
) => {
e.preventDefault();

const formData = new FormData(e.currentTarget);

const result = await createUser({
name: formData.get("name"),
email: formData.get("email"),
});

if (!result.error) {
console.log("Created:", result.data);
}
};

return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<input name="email" required />

<button disabled={isLoading}>
{isLoading ? "Creating..." : "Create"}
</button>

{isError && <div>{String(error)}</div>}
</form>
);
}

Mutation Hook Return Values

Mutation hooks return a tuple with the execution function and state object:

{
data,
isLoading,
isError,
isSuccess,
error
}

Mutation execution:

// Call with payload
const result = await execute(payload)

// Handle result
if (result.error) {
console.error('Operation failed:', result.error)
} else {
console.log('Success:', result.data)
}

Manual Refetching

Force queries to fetch fresh data by calling reFetch():

export function UsersList() {
const { data, reFetch } = useGetUsersQuery()

return (
<div>
<button onClick={() => reFetch()}>
Refresh Data
</button>
{/* Display users */}
</div>
)
}

Automatic Caching

Zustic Query automatically manages data freshness through intelligent caching:

const api = createApi({
baseQuery: myBaseQuery,
cacheTimeout: 60 * 1000, // Cache for 60 seconds
endpoints: (builder) => (/* ... */)
})

Cache Behavior:

  • Cached data reused if arguments match and cache hasn't expired
  • reFetch() always fetches fresh regardless of cache
  • Different arguments trigger separate cache entries

Advanced Endpoint Configuration

Endpoints support lifecycle hooks and transformation functions for advanced scenarios:

builder.query({
query: () => ({ url: '/users' }),

// Transform successful responses
transformResponse: (data, meta) => {
return data
},

// Transform error messages
transformError: (error, meta) => {
return error
},

// React to successful operations
onSuccess: async (data) => {
console.log('Success:', data)
},

// React to failures
onError: async (error) => {
console.error('Error:', error)
},

middlewares: [],
plugins: [],
})

Middleware & Plugins

You can configure global middleware and plugins:

createApi({
baseQuery,
middlewares: [],
plugins: [],
endpoints: (builder) => ({ ... }),
});