trpc icon indicating copy to clipboard operation
trpc copied to clipboard

`@trpc/next` with `ssr: true` breaks `getServerSideProps`

Open akomm opened this issue 4 years ago • 30 comments

Notes by @KATT:

  • Solving this is blocked by https://github.com/vercel/next.js/discussions/28183.
  • Keeping this open for visibility, but it likely won't be fixed.
  • See warning-block at https://trpc.io/docs/ssr

@trpc/next: 8.0.0-alpha.6

Using:

export default pipe(
  App,
  withTRPC({
    ssr: true,
    config: (_ctx) => ({
      url: "http://localhost:3000/api/trpc",
      queryClientConfig: {
        defaultOptions: {
          queries: {
            staleTime: 600
          }
        }
      }
    }),
  })
)

Any page with getServerSideProps, for example:

export const getServerSideProps: GetServerSideProps<Props> = async () => {
  return {
    props: {
      user: {
        name: 'x',
        email: "y"
      }
    }
  }
}

Will receive undefined props, then get refreshed with props (the latter usually never executes due to errors using undefined)

Removing withTRPC hof stops this behavior.

akomm avatar Jun 25 '21 14:06 akomm

Sounds like a bug.

On holidays for two weeks now, so if you want it fixed before that, submit a PR and I'll prob be able to review/merge/release.

I personally never use getserversideprops as it adds delay on route changes

KATT avatar Jun 26 '21 22:06 KATT

Guidance

  1. Fork trpc
  2. Add failing test / showcase it doesn't work as expected in the next-starter-prisma example
  3. Submit fix + regression test

I'll add you as a contributor so you can do it easier

KATT avatar Jun 27 '21 08:06 KATT

Can't reproduce. See 5b43446 and screenshots attached. Feel free to re-open with reproduction example.

Screenshot 2021-07-13 at 00 16 58 Screenshot 2021-07-13 at 00 17 13

KATT avatar Jul 12 '21 22:07 KATT

The test has all sort of side-effecty things, not sure why (not good if you want to reproduce something), like this:

  const postsQuery = trpc.useQuery(['posts.all']);
  const addPost = trpc.useMutation('posts.add');
  const utils = trpc.useContext();

But it does not matter, I decided not to use the library.

akomm avatar Jul 13 '21 10:07 akomm

I used an existing example to quickly assess if what you reported was a bug or not since you didn't supply a reproduction example.

Curious - what did you end up using instead of trpc?

KATT avatar Jul 13 '21 13:07 KATT

And what made you not use it?

KATT avatar Jul 13 '21 13:07 KATT

I evaluate the benefit of a library vs. cost of patching and rewiring things to own needs (plus the hidden cost to keep adjusting those adaptations with updates). Based on that I either live with things not perfectly fit and adjust them, or I do it myself, or I use alternative (in this case I just do it myself).

In general, I'd rather write some code myself than adding a library. If the cost is not high. I have lot of projects to maintain and I want it to be as simple as possible. Every thing that does not fit or has to be "adapted" artificially to match, and the adaptation looks like ugly code, is a problem I try to avoid. On top of it, you are hooked to get things fixed to release cycles or hand-wiring fix-commits in dependencies...

I figured out for my need of typed API and elimination of redundancy, it does not take much (using fp-ts/io-ts). And I don't need to create "type adapters" etc, for things to work nicely. Also: libraries will always be opinionated in some way.

As about the problem: I'd probably find the exact situation in which the problem occurs, but there are so many bugs all over the place (in other libraries), that I just don't have the time messing with all them. At times I'd jump from one bug in a package to another bug in the same package. And sometimes I can't just replace it with own code (cost to high).

The other way around examples: nextjs or pdf parser is something - to name an example - I would rather take the adjusting, than the cost of doing AND maintaining it myself.

akomm avatar Jul 13 '21 14:07 akomm

Thanks for the elaborate answer, @akomm! ❤️

I very much agree with your approach 🙏

KATT avatar Jul 13 '21 14:07 KATT

Hi,

I have the same issue trying to add TRPC to an existing App... Any clue where it could come from ?

etienne-dldc avatar Aug 01 '21 23:08 etienne-dldc

@etienne-dldc see if you can produce a reproduction using the next-prisma-starter example and open an issue!

KATT avatar Aug 02 '21 00:08 KATT

I think this is related to this https://github.com/vercel/next.js/discussions/11183

_app.getInitialProps does not receive the data returned by [some-page] getServerSideProps. This is fine for the render of the page itself because Next inject the data from [some-page] getServerSideProps before the render. But it breaks when the ssrPrepass is done since props are not there yet...

Here is a codesandbox that show the issue: https://codesandbox.io/s/vibrant-galois-rjqkc?file=/pages/_app.js (reload the page and look at the console)

etienne-dldc avatar Aug 02 '21 01:08 etienne-dldc

@etienne-dldc do you think there's anything that can be done inside of tRPC to counter it then?

Personally I never need to use getServerSideProps as I just expose whatever I want as a tRPC route that I use useQuery() on.

getServerSideProps slows down route changes, but getInitialProps doesn't.

KATT avatar Aug 02 '21 17:08 KATT

Not sure, the problem come from ssrPrepass and I'm not sure to understand what it does.

In my case I was experimenting with Trpc but I rolled back because of this issue.

getServerSideProps slows down route changes, but getInitialProps doesn't.

I'm not sure about that, can you elaborate ?

etienne-dldc avatar Aug 03 '21 07:08 etienne-dldc

@etienne-dldc even when you navigate client-side, the router still needs to execute the "getServerSideProps" on the server to get the props. Its done via a background fetch to the route, which only retrieves the result of getServerSideProps as json, instead of the whole rendered component. Then the props are populated on client-side. This trip to fetch the props and populate is what cause a delay on navigation and I guess what @KATT means.

However, getInitialProps implies using class components, which is quite an expectation. Unless I'v missed some other way around it, then please correct me.

akomm avatar Aug 04 '21 09:08 akomm

@akomm @KATT The server call to getServerSideProps does not slow down the page it only change the order in which event happen. Instead of PageChange -> Load Data you get Load Data -> PageChange.

@akomm You can use getInitialProps with function component: https://nextjs.org/docs/api-reference/data-fetching/getInitialProps

etienne-dldc avatar Aug 04 '21 12:08 etienne-dldc

@akomm @KATT The server call to getServerSideProps does not slow down the page it only change the order in which event happen. Instead of PageChange -> Load Data you get Load Data -> PageChange.

  • getServerSideProps: user clicks -> routeChangeStart --> ⏳ [..delay] --> routeChangeComplete
  • getInitialProps: user clicks -> routeChangeStart --> 🚀 instant --> routeChangeComplete

The difference is that you'll have to handle the loading client-side with getInitialProps.

For an implementation of this with tRPC doesn't require a loading you can check out https://typescript.careers which optimistically loads the data of all routes on mount (as 1 single batch request), making navigation feel instant.

If you use getStaticProps you can get instant loading as well.

See https://nextjs.org/docs/api-reference/next/router#routerevents

@akomm You can use getInitialProps with function component: https://nextjs.org/docs/api-reference/data-fetching/getInitialProps

That's what we do in tRPC - see https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L85

KATT avatar Aug 04 '21 12:08 KATT

This issue seems valid after all. Sorry for initially closing it. It's for the same as urql can't do it, see here - https://github.com/FormidableLabs/urql/discussions/1091.

I'm doing some exploratory stuff to see if there's a workaround.

KATT avatar Aug 17 '21 00:08 KATT

@KATT regarding your previous answer, why does nextjs discourage to use getInitialProps and recommends using getServerSideProps or getStaticProps instead? https://nextjs.org/docs/api-reference/data-fetching/getInitialProps

akomm avatar Aug 17 '21 10:08 akomm

Yes, for most applications this is true, but as a library author getInitialProps is super useful as it's the only thing that can do a prepass render the whole tree and do things like caching useQuery-calls, etc. And as I previously mentioned, the DX/UX you can get thanks to this is unparallel.

tRPC's plugin is largely based on urql's implementation - you can read more details about theirs here: https://formidable.com/open-source/urql/docs/advanced/server-side-rendering/

KATT avatar Aug 17 '21 11:08 KATT

Solving this is blocked by https://github.com/vercel/next.js/discussions/28183.

KATT avatar Aug 18 '21 10:08 KATT

I just encountered this issue. I wanted to use getServerSideProps for authorization but the page still gets rendered before the props gets loaded (and then re-rendered after that).

Steps to reproduce: Create a sample project npx create-next-app --example https://github.com/trpc/trpc --example-path examples/next-prisma-starter trpc-prisma-starter

Create src/pages/home.tsx

export default function Home(props) {
  console.log('Rendering with props ' + JSON.stringify(props));
  return (
    <>
      <h1>Props v</h1>
      <p>{JSON.stringify(props)}</p>
    </>
  );
}

export async function getServerSideProps(context) {
  console.log('Getting server side props');
  return {
    props: {
      firstName: 'Hello',
    },
  };
}

Going to localhost:3000/home will log the following in the server's console:

Rendering with props {}
Getting server side props
Rendering with props {"trpcState":{"json":{"mutations":[],"queries":[]}},"firstName":"Hello"}

Expected:

Getting server side props
Rendering with props {"trpcState":{"json":{"mutations":[],"queries":[]}},"firstName":"Hello"}
  • getServerSideProps gets called before the Home page is rendered. The page gets rendered with a full props object.

Actual:

  • The page is rendered with empty props, getServerSideProps is then called and the page is re-rendered with the props.
Rendering with props {}
Getting server side props
Rendering with props {"trpcState":{"json":{"mutations":[],"queries":[]}},"firstName":"Hello"}

GuiSim avatar Nov 27 '21 14:11 GuiSim

Setting ssr to false "solves" this issue.

I'm unsure what the drawbacks of this are but it seems like a workaround at the moment.

GuiSim avatar Nov 27 '21 16:11 GuiSim

I guess the query hook will only run on client and not server then. When I last read the docs it was not explained, this is just what I think it does and would make sense to me. I sometimes wish docs for components that are isomorphic clearly states "this can run ssr/client", this not. You have to guess and test often times. Sometimes its almost obvious, but you are never really sure.

akomm avatar Nov 29 '21 14:11 akomm

I'm a one man band making tRPC, all example projects, and docs.. there are limits to what I have bandwidth to do. Super keen to take on pull requests from contributors with the gaps as you learn the edge cases.

This issue of combining getSSR+getSSG and getInitialProps is out of my hands to fix as it's an issue with Next.js - if you have ideas for workarounds, they are welcome as well.

If you encounter this, set ssr: false in your _app.tsx.

KATT avatar Nov 29 '21 15:11 KATT

Note: it's still possible to do manual server-side rendering or static props with ssr: false using the getSSGHelper - https://trpc.io/docs/ssg

KATT avatar Nov 29 '21 15:11 KATT

Alex, I truly appreciate what you've done with this library and I understand that this is a limitation of Next.js.

I'm glad we have a workaround and I don't mean to put pressure on you to deliver anything here. Thanks again for the solid lib.

For me, the main value of TRPC is a typesafe API between my frontend and my backend using shared types. The rest is just gravy on top :)

GuiSim avatar Nov 29 '21 16:11 GuiSim

Worth watching:

  • Next.js "Layouts" RFC: https://nextjs.org/blog/layouts-rfc#data-fetching
  • This comment specifically: https://github.com/vercel/next.js/discussions/37136#discussioncomment-2807508

mmkal avatar Jul 18 '22 18:07 mmkal

Thank you for the this thread, I had a hard time figuring out why the props are undefined :) Could someone explain to me why ssr: false actually fixes this? I can see the server returning the html just as I would expect. I mean It's counterintuitive since we want to enable ssr, right?

apieceofbart avatar Sep 17 '22 15:09 apieceofbart

@apieceofbart there are two independent things that aren't exactly the same.

  1. Your components (JSX/TSX) can re rendered on server or client.
  2. The getServerSideProps can be used when rendering component on server or client.

IIRC, its a while back:

the option ssr in trpc only affects the rendering of JSX/TSX server-side. It uses getInitialProps. But in getInitialProps you don't get access to the result ofgetServerSideProps - this is why its currently undefined with ssr: true. When you set ssr: false you do not disable getServersideProps, so everything that comes from it as a result can be rendered server-side, once the ssr feature is disabled, because then getInitialProps is not used.

akomm avatar Sep 19 '22 08:09 akomm

@akomm Thank you for your comment - I do not fully understand it but I'll read some more about trpc. From what I understand it uses getInitialProps behind the scenes when ssr is set to true.

apieceofbart avatar Sep 21 '22 15:09 apieceofbart