remesh-query is a Remesh wrapper around TanStack Query Core.
It keeps TanStack Query's cache and observers as the source of truth, and uses Remesh to expose that state through domain queries and commands.
- Reuse
QueryClient,QueryObserver, andMutationObserverinstead of reimplementing cache behavior. - Expose a low-level bridge for direct cache operations.
- Expose higher-level Remesh factories for query and mutation modules.
- Stay framework-agnostic. This package targets
remeshcore, not React or Vue bindings.
pnpm add remesh-query remesh @tanstack/query-core rxjsFor framework bindings, install the official Remesh adapter:
pnpm add remesh-react
# or
pnpm add remesh-vueInject a shared QueryClient into a Remesh store.
import { QueryClient } from "@tanstack/query-core";
import { Remesh } from "remesh";
import { QueryClientExtern } from "remesh-query";
const queryClient = new QueryClient();
const store = Remesh.store({
externs: [QueryClientExtern.impl(queryClient)],
});If a store does not inject
QueryClientExtern.impl(queryClient), it silently falls back to a single module-level defaultQueryClient, and every such store shares that cache. Always inject one.
Low-level bridge around QueryClient cache APIs.
It currently exposes:
- queries:
QueryDataQuery,QueryStateQuery,GetQueriesDataQuery,GetMutationsQuery,GetQueryDefaultsQuery,GetMutationDefaultsQuery,IsFetchingQuery,IsMutatingQuery - commands:
SetQueryDataCommand,SetQueriesDataCommand,FetchQueryCommand,EnsureQueryDataCommand,PrefetchQueryCommand,SetQueryDefaultsCommand,SetMutationDefaultsCommand,RemoveQueriesCommand,ResetQueriesCommand,CancelQueriesCommand,InvalidateQueriesCommand,RefetchQueriesCommand,ClearQueryClientCommand
Its cache-observation effect must be running for the read queries to stay in
sync with changes made directly on QueryClient. Use
store.igniteDomain(QueryDomain()) like any other domain effect.
Creates a Remesh module backed by a TanStack QueryObserver.
It exposes:
- queries:
${name}ResultQuery,${name}DataQuery,${name}ErrorQuery,${name}StatusQuery,${name}FetchStatusQuery,${name}IsFetchingQuery,${name}QueryOptionsQuery - commands:
${name}SetOptionsCommand,${name}RefetchCommand,${name}SetDataCommand,${name}CancelCommand,${name}ResetCommand,${name}RemoveCommand
Creates a Remesh module backed by a TanStack MutationObserver.
It exposes:
- queries:
${name}ResultQuery,${name}DataQuery,${name}ErrorQuery,${name}StatusQuery,${name}VariablesQuery,${name}MutationOptionsQuery - commands:
${name}SetOptionsCommand,${name}MutateCommand,${name}ResetCommand
Every exported query and command is prefixed with the module name, so several
query and mutation modules can be merged into one Remesh domain without
collisions. Give each module in the same domain a distinct name, and keep it
PascalCase (e.g. Todo, SaveTodo) — the factories throw otherwise.
import { QueryClient } from "@tanstack/query-core";
import { Remesh } from "remesh";
import { QueryClientExtern, createRemeshMutation, createRemeshQuery } from "remesh-query";
const TodoDomain = Remesh.domain({
name: "TodoDomain",
impl: (domain) => {
const todoQuery = createRemeshQuery(domain, {
name: "Todo",
options: {
queryKey: ["todo", 1],
queryFn: async () => {
const response = await fetch("/api/todos/1");
return await response.json();
},
retry: false,
},
});
const saveTodoMutation = createRemeshMutation(domain, {
name: "SaveTodo",
options: {
mutationKey: ["save-todo"],
mutationFn: async (payload: { title: string }) => {
const response = await fetch("/api/todos", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(payload),
});
return await response.json();
},
},
});
return {
query: {
...todoQuery.query,
...saveTodoMutation.query,
},
command: {
...todoQuery.command,
...saveTodoMutation.command,
},
};
},
});
const queryClient = new QueryClient();
const store = Remesh.store({
externs: [QueryClientExtern.impl(queryClient)],
});
const todoDomain = store.getDomain(TodoDomain());
store.subscribeQuery(todoDomain.query.TodoDataQuery(), (todo) => {
console.log("todo", todo);
});
// Observer-backed modules need the domain effect to be running.
store.igniteDomain(TodoDomain());
store.send(todoDomain.command.TodoRefetchCommand());
store.send(
todoDomain.command.SaveTodoMutateCommand({
variables: {
title: "Ship remesh-query",
},
}),
);remesh-query itself is framework-agnostic. Use the official remesh-react
adapter to consume Remesh domains from React components. useRemeshDomain
automatically ignites the domain, so the query/mutation observers start running
without a manual store.igniteDomain() call.
import { QueryClient } from "@tanstack/query-core";
import { Remesh } from "remesh";
import { RemeshRoot } from "remesh-react";
import { createRoot } from "react-dom/client";
import { QueryClientExtern } from "remesh-query";
import { TodoDomain } from "./todo-domain";
import { App } from "./App";
const store = Remesh.store({
externs: [QueryClientExtern.impl(new QueryClient())],
});
createRoot(document.getElementById("root")!).render(
<RemeshRoot store={store}>
<App />
</RemeshRoot>,
);import { useRemeshDomain, useRemeshQuery, useRemeshSend } from "remesh-react";
import { TodoDomain } from "./todo-domain";
export function App() {
const todoDomain = useRemeshDomain(TodoDomain());
const send = useRemeshSend();
const todo = useRemeshQuery(todoDomain.query.TodoDataQuery());
const status = useRemeshQuery(todoDomain.query.TodoStatusQuery());
const isFetching = useRemeshQuery(todoDomain.query.TodoIsFetchingQuery());
return (
<div>
{isFetching && <p>Loading…</p>}
<p>Status: {status}</p>
<pre>{JSON.stringify(todo, null, 2)}</pre>
<button onClick={() => send(todoDomain.command.TodoRefetchCommand())}>Refresh</button>
<button
onClick={() =>
send(
todoDomain.command.SaveTodoMutateCommand({
variables: { title: "Ship remesh-query" },
}),
)
}
>
Save
</button>
</div>
);
}Use the official remesh-vue adapter. useRemeshDomain ignites the domain on
mount, and useRemeshQuery returns a reactive Ref (auto-unwrapped in Vue
templates).
import { createApp } from "vue";
import { QueryClient } from "@tanstack/query-core";
import { Remesh } from "remesh";
import { RemeshVue } from "remesh-vue";
import { QueryClientExtern } from "remesh-query";
import App from "./App.vue";
const store = Remesh.store({
externs: [QueryClientExtern.impl(new QueryClient())],
});
createApp(App).use(RemeshVue(store)).mount("#app");<script setup lang="ts">
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from "remesh-vue";
import { TodoDomain } from "./todo-domain";
const todoDomain = useRemeshDomain(TodoDomain());
const send = useRemeshSend();
const todo = useRemeshQuery(todoDomain.query.TodoDataQuery());
const status = useRemeshQuery(todoDomain.query.TodoStatusQuery());
const isFetching = useRemeshQuery(todoDomain.query.TodoIsFetchingQuery());
</script>
<template>
<div>
<p v-if="isFetching">Loading…</p>
<p>Status: {{ status }}</p>
<pre>{{ todo }}</pre>
<button @click="send(todoDomain.command.TodoRefetchCommand())">Refresh</button>
<button
@click="
send(
todoDomain.command.SaveTodoMutateCommand({
variables: { title: 'Ship remesh-query' },
}),
)
"
>
Save
</button>
</div>
</template>QueryClientremains authoritative for cache state.- Remesh stores observer snapshots and orchestration state, not a second cache.
QueryClientModulecan be used on its own when you only need cache commands and cache reads; ignite its domain to keep reads reactive to external cache changes.createRemeshQueryandcreateRemeshMutationrely on Remesh domain effects to mirror observer updates.
- Included: low-level cache bridge, query observer bridge, mutation observer bridge, Vitest integration coverage.
- Not included yet: hydrate/dehydrate helpers,
QueriesObserverhelpers, optimistic rollback workflows. - Framework adapters are not shipped by this package; use
remesh-react(React) orremesh-vue(Vue), as shown in the demos above.
vp install
vp test
vp check
vp pack