Rendering
ts// GET https://jsonplaceholder.typicode.com/todos/5 const todo = useSuspense(TodoResource.get, { id: 5 }); // GET https://jsonplaceholder.typicode.com/todos const todoList = useSuspense(TodoResource.getList); // GET https://jsonplaceholder.typicode.com/todos?userId=1 const todoListByUser = useSuspense(TodoResource.getList, { userId: 1 }); // subscriptions with polling, websockets or SSE const todo = useLive(TodoResource.get, { id: 5 }); // without fetch const todo = useCache(TodoResource.get, { id: 5 }); const todo = useQuery(Todo, { id: 5 }); // fetch without Suspense - returns { data, loading, error } const { data, loading, error } = useDLE(TodoResource.get, { id: 5 }); // subscribe without Suspense (use with useSuspense or useDLE) useSubscription(TodoResource.get, { id: 5 }); // parallel fetches with React.use() const postPromise = useFetch(PostResource.get, { id }); const commentsPromise = useFetch(CommentResource.getList, { postId: id }); const post = use(postPromise); const comments = use(commentsPromise);
For API definitions (like TodoResource), apply the skill "data-client-rest".
Mutations
tsconst ctrl = useController(); // PUT https://jsonplaceholder.typicode.com/todos/5 const updateTodo = todo => ctrl.fetch(TodoResource.update, { id }, todo); // PATCH https://jsonplaceholder.typicode.com/todos/5 const partialUpdateTodo = todo => ctrl.fetch(TodoResource.partialUpdate, { id }, todo); // POST https://jsonplaceholder.typicode.com/todos const addTodoToBeginning = todo => ctrl.fetch(TodoResource.getList.unshift, todo); // POST https://jsonplaceholder.typicode.com/todos?userId=1 const addTodoToEnd = todo => ctrl.fetch(TodoResource.getList.push, { userId: 1 }, todo); // DELETE https://jsonplaceholder.typicode.com/todos/5 const deleteTodo = id => ctrl.fetch(TodoResource.delete, { id }); // GET https://jsonplaceholder.typicode.com/todos?userId=1&page=2 const getNextPage = (page) => ctrl.fetch(TodoResource.getList.getPage, { userId: 1, page })
Helpful hooks
tsxconst [handleSubmit, loading, error] = useLoading( async data => { const post = await ctrl.fetch(PostResource.getList.push, data); navigateToPost(post.id); }, [ctrl], );
tsxconst [query, setQuery] = React.useState(''); const handleChange = e => setQuery(e.currentTarget.value); const [debouncedQuery, isPending] = useDebounce(query, 200); return ( <AsyncBoundary fallback={<Loading />}> <IssueList query={debouncedQuery} owner="facebook" repo="react" /> </AsyncBoundary> )
Components
Prefer using AsyncBoundary for error handling and loading states unless the codebase has
a custom AsyncBoundary that already combines Suspense and ErrorBoundary.
Its props are fallback, errorComponent, errorClassName, and listen. It can be used to wrap any component that fetches data.
tsx<AsyncBoundary listen={history.listen}> <TodoList /> </AsyncBoundary>
Type-safe imperative actions
Controller is returned from useController(). It has:
ctrl.fetch(), ctrl.fetchIfStale(), ctrl.expireAll(), ctrl.invalidate(), ctrl.invalidateAll(), ctrl.setResponse(), ctrl.set(),
ctrl.setError(), ctrl.resetEntireStore(), ctrl.subscribe(), ctrl.unsubscribe().
Write many entities without a fetch with one ctrl.set([Entity], rows). Never loop ctrl.set(Entity, args, row) per row, and never add an endpoint or setResponse() just to batch.
Programmatic queries
tsconst queryRemainingTodos = new Query( TodoResource.getList.schema, entries => entries.filter(todo => !todo.completed).length, ); const allRemainingTodos = useQuery(queryRemainingTodos); const firstUserRemainingTodos = useQuery(queryRemainingTodos, { userId: 1 });
tsconst groupTodoByUser = new Query( TodoResource.getList.schema, todos => Object.groupBy(todos, todo => todo.userId), ); const todosByUser = useQuery(groupTodoByUser);
Browser Debugging (Chrome DevTools MCP)
To inspect store state, track dispatched actions, or invoke
Controller methods from a browser MCP (user-chrome-devtools),
see devtools-debugging. Uses globalThis.__DC_CONTROLLERS__
available in dev mode.
Managers
Custom Managers allow for global side effect handling. This is useful for websockets, SSE, logging, etc. Always use the skill "data-client-manager" when writing managers.
Best Practices & Notes
- useDebounce(query, timeout) when rendering async data based on user field inputs
- [handleSubmit, loading, error] = useLoading() when tracking async mutations
- Prefer smaller React components that do one thing
- Co-locate data bindings: call useSuspense/useDLE/useCache/useQuery in the component that renders the data — don't prop drill
- Don't hide data bindings inside custom hooks: wrapping them obfuscates a component's data dependencies and couples data logic to view code, causing drift. Put tightly coupled data transformations in a
Queryschema (with the data model, e.g.src/resources/) so they stay reusable and evolve independently of views - For tests, apply the skill "data-client-react-testing"
References
For detailed API documentation, see the references directory:
- useSuspense;_pagination.md - Fetch with Suspense
- useFetch - Fetch for React.use() and parallel loading
- useQuery - Read from cache without fetch
- useCache - Read from cache (nullable)
- useLive;_useLive.md - Fetch + subscribe to updates
- useDLE - Fetch without Suspense (returns data/loading/error)
- useSubscription - Subscribe to updates (polling/websocket/SSE)
- useController - Access Controller
- Controller - Imperative actions
- AsyncBoundary;_AsyncBoundary.md - Error/loading boundary
- useLoading;_useLoading.md - Track async mutation state
- useDebounce - Debounce values
- DataProvider - Root provider
- data-dependency - Rendering guide
- mutations;_VoteDemo.md - Mutations guide
- Actions - Store action types (FETCH, SET, etc.)
- devtools-debugging - Debug with Chrome DevTools MCP
ALWAYS follow these patterns and refer to the official docs for edge cases. Prioritize code generation that is idiomatic, type-safe, and leverages automatic normalization/caching via skill "data-client-schema" definitions.


