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

convex-route-query

v0.1.2

Published

Type-safe helpers for prefetching Convex queries in TanStack Router loaders and reusing the typed args in route components.

Readme

convex-route-query

npm version CI MIT License

Type-safe helpers for prefetching Convex queries in TanStack Router loaders and reusing the typed args in route components.

convex-route-query lets a route loader fetch or prefetch a Convex query once, return the typed query args as loader data, and then read the same query from the route component without rebuilding params or search state. It keeps convexQuery(...) as the source of truth for query keys and stale time while giving TypeScript a stable route-data key to check.

import { createFileRoute } from "@tanstack/react-router";
import { createConvexRouteQueries } from "convex-route-query";
import { api } from "../convex/_generated/api";

const { listExperiences } = createConvexRouteQueries({
  listExperiences: api.resume.queries.listExperiences,
});

export const Route = createFileRoute("/experience/")({
  loader: async (ctx) => ({
    ...(await listExperiences.prefetchRoute(ctx)),
  }),
  component: ExperiencePage,
});

function ExperiencePage() {
  const { data } = listExperiences.useSuspenseRouteQuery(Route);

  return <ExperienceList experiences={data} />;
}

Why this exists

Convex, TanStack Router, and TanStack Query fit together beautifully, but route data loading can still get repetitive:

  • The loader has the route params, validated search params, and QueryClient.
  • The component needs the same Convex args again to call useSuspenseQuery.
  • Convex query args should stay inferred from the generated api.
  • queryKey, queryFn, and staleTime should come from convexQuery(...), not be rewritten by hand.

This package gives each query a stable typed route key:

const getPost = createConvexRouteQuery("getPost", api.blog.queries.getPost);

The loader returns the key by spreading prefetchRoute(...), and the component uses that key with useSuspenseRouteQuery(Route). TypeScript makes sure those two sides match.

Table of contents

Install

bun add convex-route-query
npm install convex-route-query

convex-route-query expects these peer dependencies to be installed in your app:

bun add @convex-dev/react-query @tanstack/react-query convex react

Before you start

This package assumes Convex is already configured with TanStack Query and that your TanStack Router context exposes a queryClient to route loaders.

If you have not wired that up yet, start with the official Convex docs:

Once that setup is in place, convex-route-query gives you route-level helpers on top of it.

At a glance

  • Prefetch or fetch Convex queries from TanStack Router loaders.
  • Return typed loader data that remembers the exact query args for the route match.
  • Read those args from the route component with useSuspenseRouteQuery(Route).
  • Keep generated Convex API types end to end.
  • Use normal TanStack Query options for non-suspense useQuery, while convex-route-query owns queryKey, queryFn, and staleTime.

Basic usage

Use createConvexRouteQueries(...) when you want keys inferred from object property names.

import { createFileRoute } from "@tanstack/react-router";
import { createConvexRouteQueries } from "convex-route-query";
import { api } from "../convex/_generated/api";

const { listPosts } = createConvexRouteQueries({
  listPosts: api.blog.queries.listPosts,
});

export const Route = createFileRoute("/blog/")({
  loader: async (ctx) => ({
    ...(await listPosts.prefetchRoute(ctx)),
  }),
  component: BlogIndexPage,
});

function BlogIndexPage() {
  const { data: posts } = listPosts.useSuspenseRouteQuery(Route);

  return <PostList posts={posts} />;
}

prefetchRoute(ctx) warms the TanStack Query cache and returns a small typed loader-data fragment. useSuspenseRouteQuery(Route) reads that fragment from Route.useLoaderData() and subscribes to the same Convex query.

Search params

When search params drive the query, put the exact Convex args in loaderDeps. TanStack Router will reload when those deps change, and prefetchRoute(ctx) can use ctx.deps automatically.

import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
import { createConvexRouteQueries } from "convex-route-query";
import { api } from "../convex/_generated/api";

const postsSearchSchema = z.object({
  page: z.number().catch(1),
  tag: z.string().catch("all"),
});

const { listPosts } = createConvexRouteQueries({
  listPosts: api.blog.queries.listPosts,
});

export const Route = createFileRoute("/blog/")({
  validateSearch: postsSearchSchema,
  loaderDeps: ({ search }) => ({
    page: search.page,
    tag: search.tag,
  }),
  loader: async (ctx) => ({
    ...(await listPosts.prefetchRoute(ctx)),
  }),
  component: BlogIndexPage,
});

function BlogIndexPage() {
  const { data: posts } = listPosts.useSuspenseRouteQuery(Route);

  return <PostList posts={posts} />;
}

The component does not call Route.useSearch() or rebuild the query args. The loader already serialized the typed query args for this route match.

Path params and loader work

Use an explicit key when you only need one query, or when you prefer to name it directly.

const getPost = createConvexRouteQuery("getPost", api.blog.queries.getPost);

For path params, or any loader that needs to combine params with other data, pass the Convex args to fetchRoute or prefetchRoute explicitly. The component still reads them from loader data.

import { createFileRoute, notFound } from "@tanstack/react-router";
import { createConvexRouteQuery } from "convex-route-query";
import { api } from "../convex/_generated/api";

const getPost = createConvexRouteQuery("getPost", api.blog.queries.getPost);

export const Route = createFileRoute("/blog/$slug")({
  loader: async (ctx) => {
    const post = await getPost.fetchRoute(ctx, {
      slug: ctx.params.slug,
    });

    if (!post.data) {
      throw notFound();
    }

    return {
      ...post.routeData,
      title: post.data.title,
    };
  },
  component: BlogPostPage,
});

function BlogPostPage() {
  const { data: post } = getPost.useSuspenseRouteQuery(Route);
  const { title } = Route.useLoaderData();

  return <Post post={post} title={title} />;
}

Your loader can keep doing normal loader work: auth, redirects, extra prefetches, metadata, and anything else. Just include the route data returned by prefetchRoute(...) or fetchRoute(...).routeData.

Non-suspense usage

Use useQuery when a component should render its own pending state.

function DraftPostPage() {
  const { slug } = Route.useParams();
  const { data: post, isPending } = getPost.useQuery(
    { slug },
    { enabled: Boolean(slug) },
  );

  if (isPending) {
    return <Spinner />;
  }

  return <PostEditor post={post} />;
}

useQuery accepts normal TanStack Query options except the generated Convex options owned by this package: queryKey, queryFn, and staleTime.

API reference

createConvexRouteQuery(query)

const query = createConvexRouteQuery(api.someModule.someQuery);

Creates the low-level helper. The runtime route key is derived from the Convex function name, but TypeScript only sees a general string key.

createConvexRouteQuery(id, query)

const getPost = createConvexRouteQuery("getPost", api.blog.queries.getPost);

Creates a route-aware helper with an explicit typed key.

createConvexRouteQueries(queries)

const { getPost, listPosts } = createConvexRouteQueries({
  getPost: api.blog.queries.getPost,
  listPosts: api.blog.queries.listPosts,
});

Creates route-aware helpers whose typed keys are inferred from the object keys.

Helpers

| Helper | Use it when | Returns | | --- | --- | --- | | options(...args) | You need the underlying convexQuery(...) options. | Convex query options | | fetchQuery(queryClient, ...args) | A loader or utility needs the query result directly. | Promise<FunctionReturnType<Query>> | | prefetchQuery(queryClient, ...args) | A loader or utility should warm the cache directly. | Promise<void> | | fetchRoute(ctx, ...args?) | A loader needs the result and a typed route-data fragment. | { data, routeData } | | prefetchRoute(ctx, ...args?) | A loader should warm the cache and return route data. | Typed route-data fragment | | useSuspenseRouteQuery(Route) | A route component should read the query args from loader data. | Suspense query result | | useQuery(...args, queryOptions) | A component wants non-suspense query state. | Query result | | useSuspenseQuery(...args) | A component already has the args available. | Suspense query result |

The route helpers need ctx.context.queryClient. If no args are passed to fetchRoute or prefetchRoute, the helper uses ctx.deps.

Notes

  • This package is intentionally small. It wraps Convex queries for route loading and component reads; mutations and actions should use the normal Convex and TanStack Query APIs.
  • The internal loader-data key is stable and typed from your explicit key or from createConvexRouteQueries(...) object keys.
  • Convex query options are generated by @convex-dev/react-query, including the query key and infinite stale time.