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) => ({ ... }),
});