Read, change and render async and shared state in React with @data-client/react: any data from an API or other async source, and global state shared across components (it has to persist somewhere, which makes it async). Use it for loading and error states, mutations and forms, filtered or derived views, and debugging data that looks wrong or stale on screen. Use it in place of fetch in useEffect, or app-wide useState and context; its main hooks are useSuspense, useQuery and useController.
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
// 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 });
// conditional: null skips fetching and binding; an endpoint without params takes {}
const user = useSuspense(UserResource.get, userId ? { id: userId } : null);
// 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);Sequential useSuspense calls waterfall, so load 2+ endpoints with useFetch as above. Before React 19,
read them with useSuspense instead of use().
For API definitions (like TodoResource), apply the skill "data-client-rest".
const 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 })const [handleSubmit, loading, error] = useLoading(
async data => {
const post = await ctrl.fetch(PostResource.getList.push, data);
navigateToPost(post.id);
},
[ctrl],
);const [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="react" repo="react" />
</AsyncBoundary>
)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.
<AsyncBoundary listen={history.listen}>
<TodoList />
</AsyncBoundary>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().
| To refresh or update | Call |
|---|---|
| You already have the new value | ctrl.set() / ctrl.setResponse() (no network) |
| One endpoint+args | ctrl.fetch(endpoint, args) |
| All cached args of an endpoint | ctrl.expireAll(endpoint): fetching hooks refetch; useCache/useQuery don't |
| Current data must not show while refetching | ctrl.invalidate() / ctrl.invalidateAll(): useSuspense suspends |
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.
const queryRemainingTodos = new Query(
TodoResource.getList.schema,
entries => entries.filter(todo => !todo.completed).length,
);
const allRemainingTodos = useQuery(queryRemainingTodos);
const firstUserRemainingTodos = useQuery(queryRemainingTodos, { userId: 1 });const groupTodoByUser = new Query(
TodoResource.getList.schema,
todos => Object.groupBy(todos, todo => todo.userId),
);
const todosByUser = useQuery(groupTodoByUser);When the running app shows the wrong data, inspect the page's store before reading code for the cause. In dev mode,
__DC__.controller is the page's Controller and __DC__.actions its recent actions, reachable
from any browser tool that runs JavaScript in the page. devtools-debugging maps
symptoms to what to check and links the setup for your browser tool.
Custom Managers allow for global side effect handling.
This is useful for websockets, SSE, logging, etc. Pass a module-level function that creates them, so each store gets its own:
const managers = () => [new MyManager(), ...getDefaultManagers()]; with
<DataProvider managers={managers}>. Always use the skill "data-client-manager" when writing managers.
Query schema (with the data model, e.g. src/resources/) so they stay reusable and evolve independently of viewsundefined from useCache, or useQuery on an Entity, means "not loaded yet", not "doesn't exist"; a deep link or refresh starts empty. Read required data with useSuspense/useDLE, which fetch only when the cache is missing, stale or invalidated; use useCache/useQuery for data other code loadsFor detailed API documentation, see the references directory:
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.
reactive/data-client
e1ea9ce
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.