Last active
October 18, 2025 11:38
-
-
Save aleclarson/914a784b7a27a2b5b66358b9f9e0622e to your computer and use it in GitHub Desktop.
Remix 3 Query primitive
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { RoutePattern } from '@remix-run/route-pattern' | |
import { queryCache } from './query.ts' | |
const userProfileRoute = new RoutePattern('/users/:id') | |
type UserProfile = { | |
name?: string | |
email?: number | |
biography?: string | |
avatar?: string | |
} | |
export function UserProfile(this: Remix.Handle) { | |
const query = queryCache(this) | |
return (props: { id: string; columns?: string[] }) => { | |
const selectedColumns = props.columns?.join(',') ?? '' | |
const { isLoading, isError, data, error } = query({ | |
queryKey: [userProfileRoute.source, props.id, selectedColumns], | |
queryFn: (signal) => | |
// In a serious project, this would be type-safe. | |
fetch( | |
userProfileRoute.href({ id: props.id }, { columns: selectedColumns }), | |
{ signal }, | |
).then((res) => res.json()) as Promise<UserProfile>, | |
}) | |
if (isLoading) { | |
data satisfies undefined | |
error satisfies undefined | |
return <div>Loading...</div> | |
} | |
if (isError) { | |
return ( | |
<div> | |
Error: {error instanceof Error ? error.message : String(error)} | |
</div> | |
) | |
} | |
data satisfies UserProfile | |
error satisfies undefined | |
return <div>User profile: {JSON.stringify(data)}</div> | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { JsonValue } from 'type-fest' | |
export function queryCache(component: Remix.Handle) { | |
const queryCache = new Map<string, RemixQuery>() | |
const cacheKeys = new Set<string>() | |
let flushScheduled = false | |
const trackQueryAccess = (cacheKey: string) => { | |
cacheKeys.add(cacheKey) | |
// Once the component update is complete, clear the cache of unused queries. | |
if (!flushScheduled) { | |
flushScheduled = true | |
component.queueTask(() => { | |
for (const query of queryCache.values()) { | |
const cacheKey = query['cacheKey'] | |
if (!cacheKeys.has(cacheKey)) { | |
query[Symbol.dispose]() | |
queryCache.delete(cacheKey) | |
} | |
} | |
cacheKeys.clear() | |
flushScheduled = false | |
}) | |
} | |
} | |
function query<T>({ | |
queryKey, | |
queryFn, | |
}: { | |
queryKey: JsonValue[] | |
queryFn: (signal: AbortSignal) => PromiseLike<T> | |
}) { | |
const cacheKey = JSON.stringify(queryKey) | |
trackQueryAccess(cacheKey) | |
const cachedQuery = queryCache.get(cacheKey) | |
if (cachedQuery) { | |
return cachedQuery as TaggedRemixQuery<T> | |
} | |
const ctrl = new AbortController() | |
const query = new RemixQuery<T>(cacheKey, ctrl, async () => { | |
if (query.isLoading) return | |
query.isLoading = true | |
try { | |
const data = await queryFn( | |
AbortSignal.any([component.signal, ctrl.signal]), | |
) | |
query.isSuccess = true | |
query.data = data | |
} catch (error) { | |
if (!isAbortError(error)) { | |
query.isError = true | |
query.error = error | |
} | |
} | |
query.isLoading = false | |
component.update() | |
}) | |
queryCache.set(cacheKey, query) | |
component.queueTask(query.refresh) | |
return query as TaggedRemixQuery<T> | |
} | |
return query | |
} | |
export class RemixQuery<T = any> { | |
constructor( | |
protected readonly cacheKey: string, | |
protected readonly ctrl: AbortController, | |
public readonly refresh: () => Promise<void>, | |
) {} | |
isLoading = false | |
isError = false | |
isSuccess = false | |
data: T | undefined | |
error: unknown; | |
[Symbol.dispose]() { | |
this.ctrl.abort() | |
} | |
} | |
export type TaggedRemixQuery<T> = RemixQuery<T> & | |
(LoadingQuery | ErrorQuery | SuccessQuery<T>) | |
type LoadingQuery = { | |
isLoading: true | |
isError: false | |
isSuccess: false | |
data: undefined | |
error: undefined | |
} | |
type ErrorQuery = { | |
isError: true | |
isLoading: false | |
isSuccess: false | |
data: undefined | |
error: unknown | |
} | |
type SuccessQuery<T> = { | |
isSuccess: true | |
isLoading: false | |
isError: false | |
data: T | |
error: undefined | |
} | |
function isAbortError(error: unknown): error is Error & { name: 'AbortError' } { | |
return error instanceof Error && error.name === 'AbortError' | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment