Tristan
Tristan5mo ago

Remix and preloadQuery or alternatives

I'm trying to experiment with ways to avoid data loading flicker in Remix app. Is there a recommended approach to server side rendering / preloading? I tried putting preloadQuery from convex/nextjs in my remix component loader but get a bunch of errors. Any recommendations? My test code is:
import type { MetaFunction } from "@remix-run/node";
import { json, useLoaderData } from "@remix-run/react";
import { api } from "convex/_generated/api";
import { usePreloadedQuery } from "convex/react";

// Only available in NextJS package but trying to use it in Remix
import { preloadQuery } from "convex/nextjs";

// Remix data loader
export async function loader() {
// Runtime Typerror: Cannot destructure property 'ENV' of '__vite_ssr_import_1__.useLoaderData(...)' as it is undefined.
// Error: Environment variable NEXT_PUBLIC_CONVEX_URL is not set. (root.ts)
const tasks = await preloadQuery(api.tasks.get);
return json({ tasks });
}

export default function Index() {
const preloaded = useLoaderData<typeof loader>();
console.log(preloaded);
// Static TypeError: Argument of type 'JsonifyObject<Preloaded<FunctionReference<"query", "public", {}, any[], string | undefined>>>' is not assignable to parameter of type 'Preloaded<FunctionReference<"query">>'.
const tasks = usePreloadedQuery(preloaded.tasks);
return <div>{JSON.stringify(tasks)}</div>;
}
import type { MetaFunction } from "@remix-run/node";
import { json, useLoaderData } from "@remix-run/react";
import { api } from "convex/_generated/api";
import { usePreloadedQuery } from "convex/react";

// Only available in NextJS package but trying to use it in Remix
import { preloadQuery } from "convex/nextjs";

// Remix data loader
export async function loader() {
// Runtime Typerror: Cannot destructure property 'ENV' of '__vite_ssr_import_1__.useLoaderData(...)' as it is undefined.
// Error: Environment variable NEXT_PUBLIC_CONVEX_URL is not set. (root.ts)
const tasks = await preloadQuery(api.tasks.get);
return json({ tasks });
}

export default function Index() {
const preloaded = useLoaderData<typeof loader>();
console.log(preloaded);
// Static TypeError: Argument of type 'JsonifyObject<Preloaded<FunctionReference<"query", "public", {}, any[], string | undefined>>>' is not assignable to parameter of type 'Preloaded<FunctionReference<"query">>'.
const tasks = usePreloadedQuery(preloaded.tasks);
return <div>{JSON.stringify(tasks)}</div>;
}
6 Replies
jamwt
jamwt5mo ago
we're focusing on next.js and then tanstack start. we want to double back to remix, but we just haven't had the bandwidth yet. anything you find out we'd love to hear about just to speed things up when we get to our remix focus
Tristan
TristanOP5mo ago
Thanks, totally understood. I was half using this to test remix. Is preloadQuery some complex thing or is it just a particular data structure for serialized data and subscription?
ballingt
ballingt5mo ago
@Tristan this should just work in Remix if you pass a Convex url option: https://docs.convex.dev/api/modules/nextjs#nextjsoptions
Module: nextjs | Convex Developer Hub
Helpers for integrating Convex into Next.js applications using server rendering.
ballingt
ballingt5mo ago
The only thing technically Next.js-specific about this is the environment variable name preloadQuery is just what it looks like, a result and a serialized query specifier
ballingt
ballingt5mo ago
We've had people use Remix with this but haven't taken on documenting support for it yet. Should be fine though, preload your queries as described in https://docs.convex.dev/client/react/nextjs/server-rendering
Next.js Server Rendering | Convex Developer Hub
Next.js automatically renders both Client and Server Components on the server
Tristan
TristanOP5mo ago
This works great, thanks. The simple fix is just to set the NEXT_PUBLIC_CONVEX_URL=$CONVEX_URL so you don't always have to manually pass it. The things that would make this smoother for Remix would be to move preloadQuery out of convex/nextjs or have a remix equivalent, and default to CONVEX_URL or both if you're keeping packages together. There is still a type error that happens when using usePreloadedQuery from the objected returned from useLoaderData but I believe this is a remix issue. Here's a working example in case it's helpful (NEXT_PUBLIC_CONVEX_URL is set in .env.local) to others in the future:
import type { LoaderFunction, TypedResponse } from "@remix-run/node";
import { json, useLoaderData } from "@remix-run/react";
import { api } from "convex/_generated/api";
import { preloadQuery } from "convex/nextjs";
import { usePreloadedQuery } from "convex/react";

// Fixes type error when passing return value of useLoaderData to usePreloadedQuery
// https://github.com/remix-run/remix/issues/8874#issuecomment-1969029983
type ExtractGeneric<Type> = Type extends TypedResponse<infer X> ? X : never;
function useDataFromLoader<T extends LoaderFunction>() {
return useLoaderData() as ExtractGeneric<Awaited<ReturnType<T>>>;
}

export async function loader() {
const tasks = await preloadQuery(api.tasks.get);
return json({ tasks });
}

export default function Index() {
const preload = useDataFromLoader<typeof loader>();
const tasks = usePreloadedQuery(preload.tasks);
return <div>{JSON.stringify(tasks)}</div>;
}
import type { LoaderFunction, TypedResponse } from "@remix-run/node";
import { json, useLoaderData } from "@remix-run/react";
import { api } from "convex/_generated/api";
import { preloadQuery } from "convex/nextjs";
import { usePreloadedQuery } from "convex/react";

// Fixes type error when passing return value of useLoaderData to usePreloadedQuery
// https://github.com/remix-run/remix/issues/8874#issuecomment-1969029983
type ExtractGeneric<Type> = Type extends TypedResponse<infer X> ? X : never;
function useDataFromLoader<T extends LoaderFunction>() {
return useLoaderData() as ExtractGeneric<Awaited<ReturnType<T>>>;
}

export async function loader() {
const tasks = await preloadQuery(api.tasks.get);
return json({ tasks });
}

export default function Index() {
const preload = useDataFromLoader<typeof loader>();
const tasks = usePreloadedQuery(preload.tasks);
return <div>{JSON.stringify(tasks)}</div>;
}

Did you find this page helpful?