Last active
November 14, 2024 18:34
-
-
Save itsMapleLeaf/d52357f17f674795a86f155f5cf80eb5 to your computer and use it in GitHub Desktop.
React suspense with Convex
This file contains 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 { Suspense } from 'react' | |
import { api } from 'convex/_generated' | |
import { useQuerySuspense } from './useQuerySuspense' | |
function App() { | |
return ( | |
<Suspense fallback="Loading..."> | |
<TodoList /> | |
</Suspense> | |
) | |
} | |
function TodoList() { | |
const todos = useQuerySuspense(api.todos.list) | |
// render todos | |
} |
This file contains 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 isDeepEqual from "fast-deep-equal" | |
import { useRef } from "react" | |
export function useMemoValue<T>( | |
value: T, | |
isEqual: (prev: T, next: T) => unknown = isDeepEqual, | |
): T { | |
const ref = useRef(value) | |
if (!isEqual(value, ref.current)) { | |
ref.current = value | |
} | |
return ref.current | |
} |
This file contains 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 { useConvex } from "convex/react" | |
import { | |
getFunctionName, | |
type FunctionReference, | |
type FunctionReturnType, | |
type OptionalRestArgs, | |
} from "convex/server" | |
import { LRUCache } from "lru-cache" | |
import { useEffect, useState } from "react" | |
import { useMemoValue } from "./useMemoValue" | |
// LRU means "last recently used" | |
// this is basically a fancy Map with a limited capacity of 100 items (or however many you want) | |
// for the sake of saving memory | |
const cache = new LRUCache<string, NonNullable<unknown>>({ | |
max: 100, | |
}) | |
function getCacheKey( | |
query: FunctionReference<"query">, | |
args: [args?: Record<string, unknown>], | |
) { | |
// JSON.stringify is basic and misses some edge cases, | |
// but is more than good enough for simple cases | |
// you might consider a stable stringifier or something like superjson instead | |
return JSON.stringify([getFunctionName(query), args]) | |
} | |
function getQueryCacheData<Query extends FunctionReference<"query">>( | |
query: Query, | |
args: OptionalRestArgs<Query>, | |
) { | |
return cache.get(getCacheKey(query, args)) as | |
| FunctionReturnType<Query> | |
| undefined | |
} | |
function setQueryCacheData<Query extends FunctionReference<"query">>( | |
query: Query, | |
args: OptionalRestArgs<Query>, | |
data: FunctionReturnType<Query>, | |
) { | |
cache.set(getCacheKey(query, args), data) | |
} | |
export function useQuerySuspense<Query extends FunctionReference<"query">>( | |
query: Query, | |
...args: OptionalRestArgs<Query> | |
) { | |
const convex = useConvex() | |
const cacheData = getQueryCacheData(query, args) | |
if (cacheData === undefined) { | |
throw new Promise<void>((resolve) => { | |
const watch = convex.watchQuery(query, ...args) | |
const result = watch.localQueryResult() | |
if (result !== undefined) { | |
setQueryCacheData(query, args, result) | |
resolve() | |
return | |
} | |
const unsubscribe = watch.onUpdate(() => { | |
const result = watch.localQueryResult() | |
if (result === undefined) { | |
throw new Error("No query result") | |
} | |
setQueryCacheData(query, args, result) | |
resolve() | |
unsubscribe() | |
}) | |
}) | |
} | |
const [data, setData] = useState(cacheData) | |
// useMemoValue makes these arguments stable for useEffect | |
const memoQuery = useMemoValue( | |
query, | |
(a, b) => getFunctionName(a) === getFunctionName(b), | |
) | |
const memoArgs = useMemoValue(args) | |
useEffect(() => { | |
const watch = convex.watchQuery(memoQuery, ...memoArgs) | |
return watch.onUpdate(() => { | |
const result = watch.localQueryResult() | |
if (result === undefined) { | |
throw new Error("No query result") | |
} | |
setData(result) | |
setQueryCacheData(memoQuery, memoArgs, result) | |
}) | |
}, [convex, memoQuery, memoArgs]) | |
return data | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment