remix icon indicating copy to clipboard operation
remix copied to clipboard

Using streaming/defer with prisma

Open JClackett opened this issue 2 years ago • 23 comments

What version of Remix are you using?

1.11.0

Steps to Reproduce

Using the new defer function with a prisma call breaks the Await component.

When logging the render prop value its just an empty object i.e {}

After a bit of debugging I noticed that the promise return type of a prisma call is a PrismaPromise, it seems the Await component doesn't like this.

Wrapping the prisma call in Promise seems to solve the issue:

  const users = new Promise(async (resolve) => {
    db.user
      .findMany()
      .then(resolve)
  })
  return defer({ otherThings, users })

Even after the fix above, I also get this error in the browser console Error: This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.

Expected Behavior

Defers loading prisma call

Actual Behavior

Blows up

JClackett avatar Jan 19 '23 23:01 JClackett

Was just about to start an issue on this, but saw that @JClackett already did.

I created a demo that replicates the error: https://github.com/xHomu/remix-defer-indie-stack/blob/main/app/routes/index.tsx

image

xHomu avatar Jan 20 '23 01:01 xHomu

I have a similar issue and the same error message. It's working great when I navigate to the page were the defer is used but I receive the same error message in the browser console when I reload the page.

I tried to reproduce the bug with a basic example but it's working as expected. I think it's something that is coming from my stack, maybe i18n...

rperon avatar Jan 20 '23 09:01 rperon

I don't use prisma or i18n but have the same issue. Even when deferring something like this for testing:

export async function getCurrentUser() {
  await new Promise((r) => setTimeout(r, 2000));
  return {
    email: "[email protected]",
    first_name: "First Name",
    last_name: "Last Name",
  };
}

adanielyan avatar Jan 21 '23 21:01 adanielyan

So I just had the same problem come up and if anyone is interested, I've been building out an ensuredPromise for any time I want to defer something... If wrapping in a promise solves the issue you can just do it once and not have to worry again:

export async function ensuredPromise<T, P extends string | number | null>(promiseFunction: (prop: NonNullable<P>) => Promise<T>, prop: P) {
	return !!prop
		? new Promise(async resolve => {
				const res = await promiseFunction(prop);
				return resolve(res);
		  })
		: (async () => null)();
}

It's messy, but i've only needed to pass a single string or number through because I make helper functions, it's also handling the scenario where you pass through a potentially undefined prop, defer always needs a promise that resolves null (undefined will break it)

TheRealFlyingCoder avatar Mar 27 '23 00:03 TheRealFlyingCoder

Any updates on this? I have to wrap every prisma call in a new Promise which also loses the type safety

JClackett avatar May 03 '23 16:05 JClackett

EDIT: Referring to this part

Error: This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.

FWIW, I'm using 1.15 with defer, and wrapping my prisma queries in a promise doesn't resolve / workaround the issue for me. On initial load, the deferred data will never resolve

I think this issue extends beyond prisma too and is also being tracked by https://github.com/remix-run/remix/issues/5760

nggonzalez avatar May 03 '23 19:05 nggonzalez

In the meanwhile, you can use either of the following:

// .then(result => result)
return defer({
  users: db.user.findMany({ ... }).then(u => u)
});
return defer({
  users: Promise.resolve().then(() => db.user.findMany({ ... })
});

Both of these should retain type information!

This likely happens because Prisma doesn't actually return a Promise until you call .then.

You could build a small wrapper for it that lets you use defer as usual:

// I have NOT tested this

import { defer as remixDefer } from "@remix-run/node";

export function defer<Data extends Record<string, unknown>>(data: Data) {
  return Object.fromEntries(
    Object.entries()(
      ([key, value]) => [key, "then" in value ? value.then(r => r) : value]
    )
  );
}

// in your loader, use as normal:
return defer({
  users: db.users.findMany({ ... })
});

merlinaudio avatar May 08 '23 14:05 merlinaudio

Just want to inform the problem still exists and it is not related to prisma. It's related to <Await> component and it's conflict with React's <Suspense> component.

mikkpokk avatar Jun 16 '23 15:06 mikkpokk

@mikkpokk Can You provide some more details or a link to a discussion about this problem? We do have the very same problem. The problem appears when a page is server side rendered and the page's loader returns deferred data. This issue appeared also in @kentcdodds "Advanced Remix" course in Frontend Masters.

gforro avatar Aug 30 '23 10:08 gforro

@gforro Unfortunately, I didn't had chance to dig deeper and I wrote my own hook instead to resolve the issue in my project.

Usage inside component:

...
const [deferred_data, deferred_loading, deferred_error] = useResolveDeferredData(data?.deferred_data)
...

Hook itself:

const isPromise = (input) => input && typeof input.then === 'function'

const useResolveDeferredData = (input, emptyDataState = {}) => {
    const [data, setData] = useState<any>(isPromise(input) ? emptyDataState : input)
    const [loading, setLoading] = useState<boolean>(isPromise(input))
    const [error, setError] = useState<string|null>(null)

    useEffect(() => {
        if (isPromise(input)) {
            setLoading(true)

            Promise.resolve(input).then(data => {
                setData(data)
                setLoading(false)
            }).catch((error) => {
                if (error.message !== 'Deferred data aborted') {
                    // This should fire only in case of unexpected or expected server error
                    setData(emptyDataState)
                    setError(error.message)
                    setLoading(false)
                }
            })
        } else {
            setData(input)
        }

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [input])

    return [
        data,
        loading,
        error,
    ]
}

export default useResolveDeferredData

mikkpokk avatar Sep 01 '23 23:09 mikkpokk

I also encountered this issue. In my case the Prisma call was in a function. Marking the function async solved it. I think it's a fair workaround for now...

martialanouman avatar Nov 17 '23 11:11 martialanouman

It’s Prisma who needs to abide to a standard Promise format. This is not a remix issue

fredericoo avatar Nov 17 '23 13:11 fredericoo

I also have this issue.

the-evgenii avatar Dec 18 '23 21:12 the-evgenii

It’s Prisma who needs to abide to a standard Promise format. This is not a remix issue.

I think it's not prisma related. A few people in this thread including myself have this issue without using prisma.

adanielyan avatar Dec 18 '23 21:12 adanielyan

It’s Prisma who needs to abide to a standard Promise format. This is not a remix issue.

I think it's not prisma related. A few people in this thread including myself have this issue without using prisma.

may I have a non-prisma example? this thread is about prisma

fredericoo avatar Dec 19 '23 11:12 fredericoo

https://github.com/remix-run/remix/issues/5153#issuecomment-1399336828 https://github.com/remix-run/remix/issues/5153#issuecomment-1594905571 https://github.com/remix-run/remix/issues/5153#issuecomment-1703494238

adanielyan avatar Dec 19 '23 16:12 adanielyan

we are facing the same issue, with mock data as described by @adanielyan

oswaldoacauan avatar Jan 11 '24 13:01 oswaldoacauan

Is there any update on this regarding non-prisma calls?

jansedlon avatar Mar 29 '24 11:03 jansedlon

If you're getting hydration issues on the initial page load, this will break defer and streaming results. This is because React re-renders your app after a hydration mismatch, so Remix no longer handles the streamed results. This affects any deferred promises, not just those from Prisma.

This is a known issue with how React 18.2 handles the hydration of the entire document vs a single div. This is primarily due to browser extensions that mutate the DOM before hydration.

The current solution is to use React Canary (currently the pre-release of v19).

Please take a look at my example. This uses the new Single Data Fetch feature (v2.9), enabling you to return promises directly (including nested promises) without using defer. It also shows how to use React Canary and overrides.

https://github.com/kiliman/remix-single-fetch

kiliman avatar Mar 29 '24 13:03 kiliman

@kiliman Hey, no, I don't have any hydration issues. What breaks it is simply using await sleep(1000) in an async function that returns DB query (no prisma)

jansedlon avatar Mar 29 '24 13:03 jansedlon

@jansedlon Do you have an example repo that I can check out?

kiliman avatar Mar 29 '24 13:03 kiliman

@kiliman Uhhh, I'll try to make one

jansedlon avatar Mar 29 '24 14:03 jansedlon

I think the issue is similar to https://github.com/remix-run/remix/issues/9440, remix judges whether the deferred data instance is Promise based on instanceof Promise, but some data may wrap Promise, resulting in the destruction of the subsequent process

That's also why we wrap it up with Promise and it solves the problem

shaodahong avatar May 21 '24 01:05 shaodahong