npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@servicetitan/tanstack-query-mobx

v5.1.0

Published

TanStack Query (React Query) integration for MobX stores with react-ioc dependency injection. Provides declarative data fetching, caching, synchronization, and state management with automatic query deduplication and observable state.

Downloads

6,168

Readme

@servicetitan/tanstack-query-mobx

TanStack Query integration for MobX stores with @servicetitan/react-ioc dependency injection. Provides declarative data fetching, caching, and state management with automatic query deduplication.

Installation

npm install @servicetitan/tanstack-query-mobx

Getting Started

1. Provide the Query Client

import { getQueryClient } from '@servicetitan/tanstack-query-mobx/composable';

export const App = provide({
    singletons: [getQueryClient()],
})(observer(() => <YourApp />));

2. Create a Store

Two approaches — both work with the same query client. The composable approach is recommended for new stores.

Composable (@servicetitan/tanstack-query-mobx/composable) — Recommended

The @queryStore decorator + query()/mutation() factories. All queries are typed class properties with consistent access patterns. Favors composition over inheritance.

import { queryStore, query, mutation } from '@servicetitan/tanstack-query-mobx/composable';

@queryStore
class JobsStore extends Store {
    @inject(JobsApi) private api?: JobsApi;
    @observable selectedJobId = 0;

    jobs = query<Job[]>(() => ({
        queryKey: ['scheduling', 'jobs'],
        queryFn: async () => (await this.api?.getJobs())?.data ?? [],
    }));

    jobDetails = query<JobDetails>(() => ({
        queryKey: ['scheduling', 'job', this.selectedJobId],
        queryFn: async () => (await this.api?.getJob(this.selectedJobId))?.data,
        enabled: this.selectedJobId > 0,
    }));

    deleteJob = mutation<void, { id: number }>(() => ({
        mutationFn: async (arg) => await this.api?.deleteJob(arg.id),
        invalidatedQueries: [['scheduling', 'jobs']],
    }));
}

Runtime queries and mutations (created after initialization, e.g., in response to user actions):

@queryStore
class DetailsStore extends Store {
    @inject(DetailsApi) private api?: DetailsApi;
    private queries = setupRuntimeQueries();

    loadDetails(id: number) {
        return this.queries.add<Details>(() => ({
            queryKey: ['details', id],
            queryFn: () => this.api?.getDetails(id),
        }), ['details', id]); // deduplicates by key
    }
}

Refresh on mount (invalidate stale queries when the store initializes):

@queryStore
class JobsStore extends Store {
    jobs = query<Job[]>(() => ({ ... }));
    techs = query<Tech[]>(() => ({ ... }));

    // Ensure fresh data from external store caches when this store mounts
    refresh = refreshOnMount(['business-units', 'list'], ['techs', 'list']);
}

Why extends Store? Stores must extend Store from @servicetitan/react-ioc to hook into the react-ioc lifecycle — initialize() runs when the provider mounts, dispose() runs when it unmounts. The @queryStore decorator uses these hooks to auto-wire queries and clean up subscriptions.

What @queryStore handles automatically:

  • Injects QueryClientStore (no @inject(QueryClientStore) needed)
  • Applies @injectable() (no separate decorator needed)
  • Auto-discovers QueryApi, MutationApi, RuntimeQueries, RuntimeMutations, and RefreshOnMountDef properties and wires their lifecycle
  • Chains consumer initialize() and dispose() methods
  • Safe with deep inheritance — prevents double setup/dispose

Inheritance (@servicetitan/tanstack-query-mobx)

Extend QueryApiStore<T> with a primary query via queryOptions getter, plus optional secondary queries via addQuery().

import { QueryApiStore, QueryApiOptions } from '@servicetitan/tanstack-query-mobx';

@injectable()
class JobsStore extends QueryApiStore<Job[]> {
    @inject(JobsApi) private api?: JobsApi;

    get queryOptions(): QueryApiOptions<Job[]> {
        return {
            queryKey: ['scheduling', 'jobs'],
            queryFn: async () => (await this.api?.getJobs())?.data ?? [],
        };
    }

    deleteJob = this.addMutation<void, { id: number }>(() => ({
        mutationFn: async (arg) => await this.api?.deleteJob(arg.id),
        invalidatedQueries: [['scheduling', 'jobs']],
    }));
}

See QueryApiStore JSDoc for full API including addQuery(), addMutation(), and lifecycle details.

3. Use in Components

const [store] = useDependencies(JobsStore);

// Composable — access via named query
const { data, isLoading } = store.jobs;

// Inheritance — primary query proxied to store
const { data, isLoading } = store;

Key Concepts

Query Keys — Arrays that uniquely identify queries. The first element is a namespace that scopes the cache (e.g., ['scheduling', 'jobs', ...]). Subsequent elements identify the resource and parameters. Changes trigger automatic refetch:

queryKey: ['scheduling', 'jobs'],           // module + resource
queryKey: ['scheduling', 'jobs', jobId],    // refetches when jobId changes

Observable State — Each query exposes data, initialized, isLoading, isFetching, isError, error

Stale Time — Default 10 minutes. Set staleTime: 0 for always fresh, Infinity for never stale.

Data-Driven Queries — MobX reactions automatically track queryKey dependencies when using a function. Use enabled to conditionally fetch:

jobs = query<Job[]>(() => ({
    queryKey: ['scheduling', 'jobs', this.filters],
    queryFn: async () => (await this.api?.getJobs(this.filters))?.data ?? [],
    enabled: this.hasRequiredData,
}));

Note: Use a function () => ({ ... }) when options depend on observables. A plain object { ... } is evaluated once at class field initialization and does not react to changes.

Extending and Overriding

Composable — shared factory functions

Share query definitions across stores with factory functions. Pass a deps function so MobX tracks observables reactively:

// shared/queries/jobs-queries.ts
export const jobsQuery = (
    deps: () => { api?: JobsApi; teamId: number }
) => query<Job[]>(() => {
    const { api, teamId } = deps();
    return {
        queryKey: ['scheduling', 'jobs', teamId],
        queryFn: async () => (await api?.getJobs(teamId))?.data ?? [],
        enabled: teamId > 0,
    };
});

// Different stores reuse the same definition
@queryStore
class SchedulingStore extends Store {
    @inject(JobsApi) private api?: JobsApi;
    @observable teamId = 0;

    jobs = jobsQuery(() => ({ api: this.api, teamId: this.teamId }));
}

Composable — @computed getter with ...super

Use a @computed getter on the base class, override in subclasses:

@queryStore
class JobsStore extends Store {
    @inject(JobsApi) protected api?: JobsApi;

    jobs = query<Job[]>(() => this.jobsQueryOptions);

    @computed
    get jobsQueryOptions(): QueryApiOptions<Job[]> {
        return {
            queryKey: ['scheduling', 'jobs'],
            queryFn: async () => (await this.api?.getJobs())?.data ?? [],
        };
    }
}

// Override the getter — same query instance, different options
@queryStore
class FilteredJobsStore extends JobsStore {
    @observable filters = {};

    get jobsQueryOptions(): QueryApiOptions<Job[]> {
        return {
            ...super.jobsQueryOptions,
            queryKey: ['scheduling', 'jobs', this.filters],
        };
    }
}

Inheritance — queryOptions getter with ...super

@injectable()
class JobsStore extends QueryApiStore<Job[]> {
    @inject(JobsApi) protected api?: JobsApi;

    get queryOptions(): QueryApiOptions<Job[]> {
        return {
            queryKey: ['scheduling', 'jobs'],
            queryFn: async () => (await this.api?.getJobs())?.data ?? [],
        };
    }
}

// Override — spread super, tweak what you need
@injectable()
class FilteredJobsStore extends JobsStore {
    @observable filters = {};

    get queryOptions(): QueryApiOptions<Job[]> {
        return {
            ...super.queryOptions,
            queryKey: ['scheduling', 'jobs', this.filters],
        };
    }
}

API Reference

Composable (@servicetitan/tanstack-query-mobx/composable)

| Export | Usage | Purpose | |--------|-------|---------| | @queryStore | Class decorator | Auto-wires lifecycle, injects QueryClientStore, applies @injectable() | | query(options) | Class fields | Create a QueryApi — plain object or () => options (use function when options depend on observables) | | mutation(options) | Class fields | Create a MutationApi — plain object or () => options (use function when options depend on observables) | | setupRuntimeQueries() | Class fields | Container for queries created after initialization — .add() and .get() | | setupRuntimeMutations() | Class fields | Container for mutations created after initialization — .add() and .get() | | refreshOnMount(...targets) | Class fields | Invalidate QueryApi instances and/or QueryKeys on store initialization |

All helpers are declarative class fields — the decorator auto-discovers and wires them. No this argument needed.

Inheritance (@servicetitan/tanstack-query-mobx)

| Export | Purpose | |--------|---------| | QueryApiStore<T> | Base class — extend and override queryOptions getter for the primary query | | QueryApi<T> | Individual query manager — data, isLoading, invalidate(), refetch(), etc. | | MutationApi<T, V> | Mutation manager — runMutation(), isPending, isSuccess, isError | | QueryApiOptions<T> | Query configuration extending TanStack's QueryObserverOptions | | MutationApiOptions<T, V> | Mutation configuration extending TanStack's MutationObserverOptions | | QueryClientStore | Manages the TanStack QueryClient instance | | getQueryClient() | Factory for providing QueryClientStore in a Provider. Pass a QueryClientConfig for custom defaults (local clients only) |

QueryApiStore provides addQuery() for secondary queries, addMutation() for mutations, and proxies primary query state (data, isLoading, initialized, etc.) to the store. See QueryApiStore JSDoc for full API.

Shared Clients (Micro-Frontends)

For micro-frontend architectures where multiple app instances share query cache:

  • 'page' — Reference-counted, disposes when last consumer unmounts
  • 'app' — Persists until browser refresh

See getQueryClient and QueryApiOptions.globalClient JSDoc.

Testing

Use ContainerBuilder from @servicetitan/tanstack-query-mobx/test — it handles QueryClientStore setup, store initialization, and waits for all queries to settle.

import { ContainerBuilder } from '@servicetitan/tanstack-query-mobx/test';

const { container, initialize } = new ContainerBuilder()
    .add(JobsStore)
    .add(JobsApi)
    .build();

jest.spyOn(container.get(JobsApi), 'getJobs').mockResolvedValue({
    data: [{ id: 1, title: 'Plumbing' }],
});

await initialize();
expect(container.get(JobsStore).jobs.data).toHaveLength(1);

See the full Testing Guide for ContainerBuilder API, skipping queries, mock values, manual setup, and query state reference.

Source Code

All classes and functions have comprehensive TSDoc comments. Explore the source for detailed documentation:

License

Part of the @servicetitan/anvil-uikit-contrib monorepo.