remix-run / remix

Build Better Websites. Create modern, resilient user experiences with web fundamentals.
https://remix.run
MIT License
29.94k stars 2.52k forks source link

Using streaming/defer with prisma #5153

Open JClackett opened 1 year ago

JClackett commented 1 year ago

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

xHomu commented 1 year ago

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

rperon commented 1 year ago

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...

adanielyan commented 1 year ago

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: "example@example.com",
    first_name: "First Name",
    last_name: "Last Name",
  };
}
TheRealFlyingCoder commented 1 year ago

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)

JClackett commented 1 year ago

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

nggonzalez commented 1 year ago

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

merlindru commented 1 year ago

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({ ... })
});
mikkpokk commented 1 year ago

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.

gforro commented 1 year ago

@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.

mikkpokk commented 1 year ago

@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
martialanouman commented 11 months ago

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...

fredericoo commented 11 months ago

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

the-evgenii commented 10 months ago

I also have this issue.

adanielyan commented 10 months ago

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.

fredericoo commented 10 months ago

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

adanielyan commented 10 months ago

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

oswaldoacauan commented 10 months ago

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

jansedlon commented 7 months ago

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

kiliman commented 7 months ago

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

jansedlon commented 7 months ago

@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)

kiliman commented 7 months ago

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

jansedlon commented 7 months ago

@kiliman Uhhh, I'll try to make one

shaodahong commented 5 months ago

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