React Query
TanStack Query v5 β Fetcher β Repository β Queries β Controller β UI
When you enable React Query, AtomKit scaffolds a full 5-layer architecture matching production patterns. Each layer has a single responsibility β from the HTTP client all the way to the presentational UI.
fetcher, items repository, QueryProvider, itemsQueries, ItemsController, AllItemsUI, and an example page wiring them together. Extract the zip and run npm install to get started.What AtomKit Generates
src/
βββ common/
β βββ fetcher.js β singleton HTTP client (getData, postData, deleteDataβ¦)
β
βββ repositories/
β βββ items.js β Items service class β wraps fetcher with /api/items endpoint
β
βββ providers/
β βββ QueryProvider.jsx β wraps app with QueryClient + Devtools
β
βββ queries/
β βββ itemsQueries.js β React Query hooks calling the repository
β
βββ controller/
β βββ ItemsController.jsx β consumes queries, manages state, cloneElement to children
β
βββ components/
β βββ pages/
β βββ items/
β βββ AllItemsUI.jsx β pure presentational component
β
βββ app/
βββ items/
β βββ page.jsx β example page: Controller + UI wired together
βββ layout.jsx β updated with <QueryProvider>The 5-Layer Architecture
Singleton HTTP client β getData, postData, deleteData.
common/fetcher.jsService class per resource β wraps fetcher with endpoints.
repositories/items.jsOne file per feature β React Query hooks call the repository.
queries/itemsQueries.jsConsume queries, manage state, cloneElement to children.
controller/ItemsController.jsxPure components β receive props, render JSX, zero data logic.
components/pages/items/AllItemsUI.jsxFetcher Repository Queries Controller UI ββββββββββββββ ββββββββββββββββ ββββββββββββββββββ βββββββββββββββββ ββββββββββββββββ β fetcher.js ββββ items.js ββββ itemsQueries ββββ ItemsControllerβββΆβ AllItemsUI β β β β β β β β β β β β getData() β β getAllItems() β β useItemsGet.. β β consumes hooksβ β receives all β β postData() β β postItem() β β useItemsPost.. β β manages state β β as props β β deleteData β β deleteItem() β β useItemsDel.. β β cloneElement β β renders JSX β ββββββββββββββ ββββββββββββββββ ββββββββββββββββββ βββββββββββββββββ ββββββββββββββββ
Layer 1 β common/fetcher.js
Singleton HTTP client. All HTTP calls go through here β one place to add auth headers, token refresh, error handling, or request logging.
class Fetcher {
static instance = null;
static getInstance() {
if (!Fetcher.instance) {
Fetcher.instance = new Fetcher();
}
return Fetcher.instance;
}
async sendRequest(url, options = {}) {
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json', ...options.headers },
...options,
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
const data = await res.json();
return { data, status: res.status };
}
async getData(url) { return this.sendRequest(url); }
async postData(url, body) { return this.sendRequest(url, { method: 'POST', body: JSON.stringify(body) }); }
async patchData(url, body) { return this.sendRequest(url, { method: 'PATCH', body: JSON.stringify(body) }); }
async putData(url, body) { return this.sendRequest(url, { method: 'PUT', body: JSON.stringify(body) }); }
async deleteData(url) { return this.sendRequest(url, { method: 'DELETE' }); }
}
export const createFetcher = () => Fetcher.getInstance();Layer 2 β repositories/items.js
One file per resource. Exports named functions that wrap the fetcher with endpoint-specific logic. Queries import these directly β IDE intellisense resolves every function.
import { createFetcher } from '@/common/fetcher';
const fetcher = createFetcher();
const endpoint = '/api/items';
export async function getAllItems(params = {}) {
const qs = new URLSearchParams(params).toString();
const url = qs ? `${endpoint}?${qs}` : endpoint;
const response = await fetcher.getData(url);
return response?.data;
}
export async function postItem(payload) {
const response = await fetcher.postData(endpoint, payload);
return response?.data;
}
export async function deleteItem(id) {
const response = await fetcher.deleteData(`${endpoint}/${id}`);
return response?.data;
}repositories/users.js with getAllUsers(), postUser(), etc. All share the same fetcher singleton.Layer 3 β queries/itemsQueries.js
One file per feature. Hooks call the repository β never fetch() directly. Naming: use[Feature][Method]Query.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getAllItems, postItem, deleteItem } from '@/repositories/items';
// βββ GET ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function useItemsGetQuery(params = {}, options = {}) {
return useQuery({
queryKey: ['itemsGetQuery', params],
queryFn: () => getAllItems(params),
staleTime: 0,
gcTime: 0,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
...options,
});
}
// βββ POST βββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function useItemsPostQuery() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newItem) => postItem(newItem),
retry: false,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['itemsGetQuery'] });
},
});
}
// βββ DELETE βββββββββββββββββββββββββββββββββββββββββββββββββββ
export function useItemsDeleteQuery() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id) => deleteItem(id),
retry: false,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['itemsGetQuery'] });
},
});
}Layer 4 β controller/ItemsController.jsx
Consumes query hooks, manages local state (search, pagination), handles events, and passes everything to a single child via Children.only(cloneElement(children, ...)).
'use client';
import { Children, cloneElement, useEffect, useState } from 'react';
import {
useItemsGetQuery,
useItemsPostQuery,
useItemsDeleteQuery,
} from '@/queries/itemsQueries';
export function ItemsController({ children, ...props }) {
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const items = useItemsGetQuery(
{ search, page: String(page), limit: '20' },
{ enabled: true }
);
const postToItems = useItemsPostQuery();
const deleteItem = useItemsDeleteQuery();
useEffect(() => {
if (postToItems.isSuccess) { setSearch(''); setPage(1); }
}, [postToItems.isSuccess]);
const handleCreate = (name) => postToItems.mutate({ name });
const handleDelete = (id) => deleteItem.mutate(id);
const handleSearch = (value) => { setSearch(value); setPage(1); };
return Children.only(
cloneElement(children, {
items, postToItems, deleteItem,
page, search,
onSearch: handleSearch,
onPageChange: setPage,
onCreate: handleCreate,
onDelete: handleDelete,
...props,
})
);
}Layer 5 β UI Component + Page
The UI component is pure β receives all data as props, renders JSX, no data logic. The page wires Controller + UI together.
import { ItemsController } from '@/controller/ItemsController';
import AllItemsUI from '@/components/pages/items/AllItemsUI';
export default function ItemsPage(props) {
return (
<ItemsController {...props}>
<AllItemsUI />
</ItemsController>
);
}export default function AllItemsUI({
items, // query object β items.data, items.isLoading, items.isError
postToItems, // mutation β postToItems.isPending
deleteItem, // mutation β deleteItem.isPending
search, page,
onSearch, onPageChange, onCreate, onDelete,
}) {
return (
<div>
<input value={search} onChange={(e) => onSearch(e.target.value)} placeholder="Searchβ¦" />
{items.isLoading && <p>Loadingβ¦</p>}
{items.isError && <p>Error: {items.error.message}</p>}
{items.data && (
<ul>
{items.data.map((item) => (
<li key={item.id}>
{item.name}
<button onClick={() => onDelete(item.id)}>Delete</button>
</li>
))}
</ul>
)}
<button onClick={() => onCreate('New Item')} disabled={postToItems.isPending}>
{postToItems.isPending ? 'Creatingβ¦' : '+ Add Item'}
</button>
</div>
);
}Adding a New Feature
Follow the same 5-layer pattern for every new resource:
Key Concepts
queryKeyUnique array identifying cached data. Include filter params to scope: ['itemsGetQuery', { search }].
staleTime / gcTimestaleTime: how long before refetch is eligible. gcTime: how long unused cache survives. Both set to 0 by default.
invalidateQueriesMarks queries as stale after a mutation, triggering a background refetch to keep UI in sync.
DevtoolsFloating panel (bottom-right) showing all active queries, cache status, and data. Click the TanStack logo to open.