Skip to content

Repository files navigation

remesh-query

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.

Goals

  • Reuse QueryClient, QueryObserver, and MutationObserver instead 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 remesh core, not React or Vue bindings.

Install

pnpm add remesh-query remesh @tanstack/query-core rxjs

For framework bindings, install the official Remesh adapter:

pnpm add remesh-react
# or
pnpm add remesh-vue

API

QueryClientExtern

Inject 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 default QueryClient, and every such store shares that cache. Always inject one.

QueryClientModule(domain)

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.

createRemeshQuery(domain, config)

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

createRemeshMutation(domain, config)

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.

Usage

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",
    },
  }),
);

Usage with React

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.

App entry (main.tsx)

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>,
);

Component (App.tsx)

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>
  );
}

Usage with Vue

Use the official remesh-vue adapter. useRemeshDomain ignites the domain on mount, and useRemeshQuery returns a reactive Ref (auto-unwrapped in Vue templates).

App entry (main.ts)

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");

Component (App.vue)

<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>

Architecture Notes

  • QueryClient remains authoritative for cache state.
  • Remesh stores observer snapshots and orchestration state, not a second cache.
  • QueryClientModule can 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.
  • createRemeshQuery and createRemeshMutation rely on Remesh domain effects to mirror observer updates.

Current Scope

  • Included: low-level cache bridge, query observer bridge, mutation observer bridge, Vitest integration coverage.
  • Not included yet: hydrate/dehydrate helpers, QueriesObserver helpers, optimistic rollback workflows.
  • Framework adapters are not shipped by this package; use remesh-react (React) or remesh-vue (Vue), as shown in the demos above.

Development

vp install
vp test
vp check
vp pack

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages