gwilliamnn
gwilliamnnâ€ĸ2y ago

Suspense or loading state

Convex has any loading state or works with suspense or something like?
15 Replies
Michal Srb
Michal Srbâ€ĸ2y ago
useQuery returns undefined while the data is being loaded. Check out https://docs.convex.dev/client/react#fetching-data
Convex React | Convex Developer Hub
Convex React is the client library enabling your React application to interact
Michal Srb
Michal Srbâ€ĸ2y ago
You could implement a wrapper hook on top useQuery that works with Suspense (ref with a promise, resolve it when the data is no longer undefined). I don't think we have a demo for this yet.
gwilliamnn
gwilliamnnOPâ€ĸ2y ago
interesting...
MapleLeaf 🍁
MapleLeaf 🍁â€ĸ17mo ago
dredging this up because I was just going to make a suggestion for this! In the meantime I handrolled my own hacky version:
export function useQuerySuspense<Query extends FunctionReference<"query">>(
query: Query,
...args: OptionalRestArgs<Query>
) {
const convex = useConvex()
const watch = convex.watchQuery(query, ...args)
const initialValue = watch.localQueryResult()

if (initialValue === undefined) {
throw new Promise<void>((resolve) => {
watch.onUpdate(() => {
resolve()
})
})
}

const [value, setValue] = useState(initialValue)

useEffect(() => {
const updateValueFromQueryResult = () => {
const value = watch.localQueryResult()
if (value === undefined) throw new Error("No query result")
setValue(value)
}

updateValueFromQueryResult()
return watch.onUpdate(updateValueFromQueryResult)
}, [watch])

return value
}
export function useQuerySuspense<Query extends FunctionReference<"query">>(
query: Query,
...args: OptionalRestArgs<Query>
) {
const convex = useConvex()
const watch = convex.watchQuery(query, ...args)
const initialValue = watch.localQueryResult()

if (initialValue === undefined) {
throw new Promise<void>((resolve) => {
watch.onUpdate(() => {
resolve()
})
})
}

const [value, setValue] = useState(initialValue)

useEffect(() => {
const updateValueFromQueryResult = () => {
const value = watch.localQueryResult()
if (value === undefined) throw new Error("No query result")
setValue(value)
}

updateValueFromQueryResult()
return watch.onUpdate(updateValueFromQueryResult)
}, [watch])

return value
}
function Example() {
const data = useQuerySuspense(api.some.query)
// ...
}

function Parent() {
return (
<React.Suspense fallback="Loading...">
<Example />
</React.Suspense>
)
}
function Example() {
const data = useQuerySuspense(api.some.query)
// ...
}

function Parent() {
return (
<React.Suspense fallback="Loading...">
<Example />
</React.Suspense>
)
}
ballingt
ballingtâ€ĸ17mo ago
This is great. Thought while reading it: - queries that return falsey values like 0 or false or "" won't work here, should be === undefined - the order in which watch.onUpdate(cb) callbacks are called I think is the opposite of what you'd hope here (the resolve() onUpdate runs before the setState onUpdate). I don't know suspense well enough to say for sure but I bet that's fine? dunno if resolving the promise synchronously causes React to render and the main thought while reading it is "we should have a demo of this hook"
MapleLeaf 🍁
MapleLeaf 🍁â€ĸ17mo ago
the order in which watch.onUpdate(cb) callbacks are called I think is the opposite of what you'd hope here (the resolve() onUpdate runs before the setState onUpdate). I don't know suspense well enough to say for sure but I bet that's fine? dunno if resolving the promise synchronously causes React to render
that might be a part of an issue I was running into, I had to update the useEffect to set the state whenever the watch changes:
useEffect(() => {
const updateValueFromQueryResult = () => {
const value = watch.localQueryResult()
if (value === undefined) throw new Error("No query result")
setValue(value)
}

updateValueFromQueryResult()
return watch.onUpdate(updateValueFromQueryResult)
}, [watch])
useEffect(() => {
const updateValueFromQueryResult = () => {
const value = watch.localQueryResult()
if (value === undefined) throw new Error("No query result")
setValue(value)
}

updateValueFromQueryResult()
return watch.onUpdate(updateValueFromQueryResult)
}, [watch])
this fixed state being stale when the query args changes, but it may fix the problem you're talking about too the new Promise callback runs synchronously though, so this is basically an onUpdate subscription immediately after a local query check
rutenisraila
rutenisrailaâ€ĸ17mo ago
Yeah would also love to see an official implementation of {isLoading} state or an official suspense solution. Checking if data is undefined is not always enough to cover all UX patterns. Sometimes you need loading state, empty state and a state with returned items
rutenisraila
rutenisrailaâ€ĸ17mo ago
this is a solution I came up with:
No description
Michal Srb
Michal Srbâ€ĸ17mo ago
@rutenisraila this is how you can cover all the cases: īģŋ
// data hasn't been loaded yet
const isLoading = data === undefined;

// your function returned undefined or null,
// or didn't `return` at all (which is the same as returning undefined)
const isNone = data === null;

// your function returned an empty array
const isEmpty = data?.length === 0;

// your function returned a non empty array
const isNonEmpty = data?.length > 0;
// data hasn't been loaded yet
const isLoading = data === undefined;

// your function returned undefined or null,
// or didn't `return` at all (which is the same as returning undefined)
const isNone = data === null;

// your function returned an empty array
const isEmpty = data?.length === 0;

// your function returned a non empty array
const isNonEmpty = data?.length > 0;
We'll work this into the docs, thanks for the feedback!
rutenisraila
rutenisrailaâ€ĸ17mo ago
thanks @Michal Srb !
winsoroaks
winsoroaksâ€ĸ12mo ago
was thinking about this too and found this thread. i'm finding myself repeating the following pattern quite often 1. check if loading 2. check if null 3. check if user is authorized 4. finally return
return (
<div className="mt-3">
{isLoading ? (
<FormWrapperLoading />
) : job === null ? (
<NotFound pageName="new job" redirectUrl="/jobs/new" />
) : canWrite ? (
<Authenticated>
<EditJobId job={job} />
</Authenticated>
) : (
<NotAuthorized />
)}
</div>
)
return (
<div className="mt-3">
{isLoading ? (
<FormWrapperLoading />
) : job === null ? (
<NotFound pageName="new job" redirectUrl="/jobs/new" />
) : canWrite ? (
<Authenticated>
<EditJobId job={job} />
</Authenticated>
) : (
<NotAuthorized />
)}
</div>
)
i think my implementation could be the easiest. but pls lemme know if i can make this DRYer
Michal Srb
Michal Srbâ€ĸ12mo ago
Depends on your app! You could perform the canWrite check higher up in the tree. You probably don't need Authenticated (if it's the one from convex/react) if you're already checking the auth on the server.
winsoroaks
winsoroaksâ€ĸ12mo ago
oh thanks! interesting. is it possible to do canWrite higher up in the tree if i want to check permission per page? here's the snippet i have.
export function useUserAuthz(
page?: (typeof PERMISSIONS)[number]
) {
const { isLoaded } = useAuth()

const userPermissions = useStableQuery(
api.db.user.getUserPermissions,
isLoaded
? {
page: page,
}
: "skip"
)

if (!isLoaded || userPermissions?.user === undefined) {
return { isLoading: true }
}
return {
isLoading: false,
userId: userPermissions.user._id,
canRead: userPermissions.canRead,
canWrite: userPermissions.canWrite ?? false,
}
}
export function useUserAuthz(
page?: (typeof PERMISSIONS)[number]
) {
const { isLoaded } = useAuth()

const userPermissions = useStableQuery(
api.db.user.getUserPermissions,
isLoaded
? {
page: page,
}
: "skip"
)

if (!isLoaded || userPermissions?.user === undefined) {
return { isLoading: true }
}
return {
isLoading: false,
userId: userPermissions.user._id,
canRead: userPermissions.canRead,
canWrite: userPermissions.canWrite ?? false,
}
}
on a side note, can i get the permissions info on the server side? my guess is no 😅
ian
ianâ€ĸ12mo ago
server-side as in your convex function? Just call the function api.db.user.getUserPermissions. Next.js server-side? You can use a convex client from there if you can setAuth server-side (if you have access to the auth token server-side)
winsoroaks
winsoroaksâ€ĸ12mo ago
yea was referring to next.js server side. ok good idea! lemme poke around. thanks 🙂

Did you find this page helpful?