⚑

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.

πŸ“¦AtomKit generates 7 files inside the downloaded zip: 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

πŸ“„Generated files (React Query enabled)
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

Fetcher

Singleton HTTP client β€” getData, postData, deleteData.

common/fetcher.js
Repository

Service class per resource β€” wraps fetcher with endpoints.

repositories/items.js
Queries

One file per feature β€” React Query hooks call the repository.

queries/itemsQueries.js
Controller

Consume queries, manage state, cloneElement to children.

controller/ItemsController.jsx
UI

Pure components β€” receive props, render JSX, zero data logic.

components/pages/items/AllItemsUI.jsx
πŸ“„Data flow
  Fetcher          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.

πŸ“„src/common/fetcher.js
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.

πŸ“„src/repositories/items.js
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;
}
πŸ’‘To add a new resource: Create 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.

πŸ“„src/queries/itemsQueries.js
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, ...)).

πŸ“„src/controller/ItemsController.jsx
'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.

πŸ“„src/app/items/page.jsx β€” the page
import { ItemsController } from '@/controller/ItemsController';
import AllItemsUI from '@/components/pages/items/AllItemsUI';

export default function ItemsPage(props) {
    return (
        <ItemsController {...props}>
            <AllItemsUI />
        </ItemsController>
    );
}
πŸ“„src/components/pages/items/AllItemsUI.jsx β€” the UI
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:

✦
1. Repository β€” repositories/users.js β€” Users class with getAllUsers(), postUser(), deleteUser(). Uses the shared fetcher.
✦
2. Queries β€” queries/usersQueries.js β€” useUsersGetQuery, useUsersPostQuery, useUsersDeleteQuery. All hooks call the repository.
✦
3. Controller β€” controller/UsersController.jsx β€” consumes queries, manages state, Children.only(cloneElement(children, ...)).
✦
4. UI component β€” components/pages/users/AllUsersUI.jsx β€” pure component, receives query objects + handlers as props.
✦
5. Page β€” app/users/page.jsx β€” <UsersController><AllUsersUI /></UsersController>.

Key Concepts

queryKey

Unique array identifying cached data. Include filter params to scope: ['itemsGetQuery', { search }].

staleTime / gcTime

staleTime: how long before refetch is eligible. gcTime: how long unused cache survives. Both set to 0 by default.

invalidateQueries

Marks queries as stale after a mutation, triggering a background refetch to keep UI in sync.

Devtools

Floating panel (bottom-right) showing all active queries, cache status, and data. Click the TanStack logo to open.

Best Practices

✦
Fetcher is the single HTTP layer β€” All requests go through the fetcher singleton. Add auth headers, token refresh, or error handling in one place.
✦
One repository per resource β€” Items, Users, Teams β€” each gets its own class wrapping the fetcher with resource-specific endpoints.
✦
One queries file per feature β€” Group all hooks (GET, POST, DELETE) for a feature in one file β€” e.g. itemsQueries.js.
✦
Naming: use[Feature][Method]Query β€” useItemsGetQuery, useItemsPostQuery, useItemsDeleteQuery β€” consistent and discoverable.
✦
Queries never call fetch() directly β€” Hooks call the repository. The repository calls the fetcher. Clean separation.
✦
Controller owns the state β€” Search, filters, pagination β€” all live in the controller, never in UI components.
✦
Children.only + cloneElement β€” Controllers pass data to a single child via cloneElement β€” keeps them composable and testable.
✦
UI stays pure β€” No useQuery, no fetch, no side-effects inside presentational components. Only receive props and render.
πŸ› Open your scaffolded app and click the TanStack logo in the bottom-right corner to open the React Query Devtools. Inspect every query's cache, status, data, and trigger manual refetches.