Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

compat with react 19 #2756

Closed
joacub opened this issue May 19, 2024 · 61 comments · Fixed by #2941
Closed

compat with react 19 #2756

joacub opened this issue May 19, 2024 · 61 comments · Fixed by #2941

Comments

@joacub
Copy link

joacub commented May 19, 2024

React pdf is not compatible with react 19, will you make the library compat with this version ?

@joacub
Copy link
Author

joacub commented May 23, 2024

👀

@ZipBrandon
Copy link

Anything @eps1lon you can suggest? I have been poking around in the react-reconciler but it has drifted and largely undocumented.

@eps1lon
Copy link

eps1lon commented May 30, 2024

react-three-fiber also had to update its reconciler implementation. One of the breaking changes that was hard to track down was around commitUpdate which is no longer called with an updatePayload.

Where is the code for the reconciler implementation? Maybe something stands out to me. But no promises for now since we're not using that library at Vercel.

@ZipBrandon
Copy link

@eps1lon Here is where @diegomura creates the reconciler. When I updated the deps on my fork, I couldn't get the document to be created from the updateContainer call here

@joacub
Copy link
Author

joacub commented Jun 3, 2024

Some advances ?

@ZipBrandon
Copy link

FWIW I have patched my version to use React 18. I only use the node version of the renderer so having a version of react@18.3.1 that is only used for that suits my purpose.

I'm aliasing react-18 with "react-18": "npm:react@^18.3.1" in my package.json.

This uses a patch I made with patch-package where the renderer code thenimport React from "react-18".

I had to use the escape hatch of "use no memo" in my components to opt-out the compiler.

I use the pragma /** @jsxImportSource react-18 */ at the top of all files that are associated with the PDF rendering so it goes through the correct createElement().

That being said, this is just duct tape to get by until we have true React 19 compatability.

@cipriancaba
Copy link

That's a very interesting aproach @ZipBrandon but I feel like vercel and react will need to come up with a better solution than this since any 3rd party lib you might be using that is not updated will simply prevent you from upgrading to next 15 and react 19.

What is the suggested path here @eps1lon ? I've spent the time migrating to next 15, loved the wait everything felt until one of our agents told me the pdf generation page was buggered so had to revert to latest stable next

@cipriancaba
Copy link

Hi @ZipBrandon , do you mind sharing the patch you used to make this work?

@cipriancaba
Copy link

Thanks for the reply @eps1lon and your efforts are obviously much appreciated

I've been tracking your issue here eps1lon/react#10

I guess when I meant suggested path I meant more like this is literally a blocker for any kind of upgrade to react 19 / next 15. Is there potential for a more generic temporary fix (maybe similar to what @ZipBrandon suggested) until react 19 is fully adopted by lib authors? In every real life project there's this obscure library that you absolutely have to use and then if that blocks your entire upgrade path, you're basically stuck.

@ZipBrandon
Copy link

@cipriancaba Sure! Here's the the gist of the patch for @react-pdf/renderer https://gist.github.com/ZipBrandon/b6c3848a400feff9741d739b7544d4fe. This is only for rendering on the Node side. I didn't do anything for browser rendering.

Caveats to remember:

  • "react-18": "npm:react@^18.3.1", in your package.json deps.
  • /** @jsxImportSource react-18 */ at the top of any of your files that are React components that are being rendered in the PDF. You should not have React 19 and React 18 components in the same file.
  • Import anything you need in those components from React from "react-18" package.
  • If using the React Compiler, opt-out in the PDF components with "use no memo"

@alexandernanberg
Copy link
Contributor

alexandernanberg commented Jun 12, 2024

I've done some investigations and the library is incompatible with react-reconciler 0.27.0 and later (React 19 requires ^0.31.0).

The library expect reconciler.createContainer to mutate the document property of the container.

const container = { type: 'ROOT', document: null };
renderer = renderer || createRenderer({ onChange });
const mountNode = renderer.createContainer(container);

which is then accessed here

const props = container.document.props || {};
const { pdfVersion, language, pageLayout, pageMode } = props;

In 0.27.0 the container.document is always null

I've tried finding workarounds, but I'm not familiar with the internals of the reconciler. @eps1lon thankful for any pointers!

Edit:

Seems like newer version of the reconciler forces the container to be immutable, or that it clones the object passed into createContainer. The mutation part is done in appendChildToContainer

appendChildToContainer(parentInstance, child) {
if (parentInstance.type === 'ROOT') {
parentInstance.document = child;
} else {
appendChild(parentInstance, child);
}
},

Edit 2:

Okay so looks like updateContainer now schedules an update instead of doing it sync. And library code relies on the sync behavior.

  const updateContainer = (doc, callback) => {
    renderer.updateContainer(doc, mountNode, null, callback);
  };

  if (initialValue) updateContainer(initialValue);

  setTimeout(() => {
    console.log(container);
	// container.document is set now
  }, 0);

@alexandernanberg
Copy link
Contributor

alexandernanberg commented Jun 12, 2024

Got it working in #2783.

If you want to try it out early you can:

  1. Install @alexandernanberg/react-pdf-renderer
  2. Replace all references of @react-pdf/renderer with alexandernanberg/react-pdf-renderer

@karlhorky
Copy link
Contributor

karlhorky commented Jun 12, 2024

@alexandernanberg thanks for this!

Tried @alexandernanberg/react-pdf-renderer out just now and it works in some circumstances!

However, it is hanging with our Express app which uses:

  • custom fonts (this seems to be the problem)
    • Font.register() with src set to an https:// URL of a WOFF font
    • StyleSheet.create() with fontFamily set to a custom font which uses Font.register()
  • (await renderToStream()).pipe(response)

We do also have this pragma, not sure if that's relevant:

/** @jsxRuntime automatic @jsxImportSource react */

@alexandernanberg
Copy link
Contributor

@karlhorky Custom fonts and stylesheets work for me, although I'm using Next.js.

I suspect that I broke something in this commit ed62fa6 (#2783). I'll see if I can revert those changes

@karlhorky
Copy link
Contributor

karlhorky commented Jun 13, 2024

Custom fonts and stylesheets work for me

Interesting, are you also using the same configuration?

  • Font.register() with src of:
    • https:// URL
    • WOFF font
  • StyleSheet.create() with fontFamily

If you could post an example of your code, then that would be helpful to track down working / not working configurations.

I'll see if I can revert those changes

Thanks!

@alexandernanberg
Copy link
Contributor

Only .ttf fonts are supported AFAIK https://react-pdf.org/fonts

Example
import {
  Document,
  Font,
  Page,
  StyleSheet,
  View,
  renderToStream,
} from "@alexandernanberg/react-pdf-renderer";

const fontFamily = {
  sans: "Inter",
};

Font.register({
  family: fontFamily.sans,
  fonts: [
    {
      src: "https://storage.googleapis.com/.../fonts/Inter-Regular.ttf",
      fontWeight: "normal",
    },
  ],
});

export function renderCertificate() {
  return renderToStream(<Certificate />);
}

export function Certificate() {
  return (
    <Document>
      <Page style={styles.page}>
        <View style={styles.container}></View>
      </Page>
    </Document>
  );
}

const styles = StyleSheet.create({
  text: {
    fontFamily: fontFamily.sans,
    fontWeight: "normal",
    fontSize: 14,
  },
});

@karlhorky
Copy link
Contributor

Only .ttf fonts are supported AFAIK

WOFF fonts are also supported, this is just undocumented. I've opened a PR to document that here:

@karlhorky
Copy link
Contributor

karlhorky commented Jun 13, 2024

Interesting, WOFF is not the problem after all, this .woff font works from Google Fonts (Inter font):

Font.register({
  family: 'Inter',
  format: 'woff',
  src: 'https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuGKYAZ9hjp-Ek-_EeA.woff',
});

All fonts on Google Fonts seem to work.

It seems to be a problem with my own font files (both TTF and WOFF) being unusable in some way. Maybe something in some dependency is being more strict with certain font files.

@karlhorky
Copy link
Contributor

karlhorky commented Jun 13, 2024

Ohh looks like this hanging behavior is a known problem with custom fonts and react-pdf and fontkit (caused by the transitive dependency restructure@3.0.1):

Downgrading to restructure@3.0.0 as mentioned in the issues above resolves the issue.


Thus, I can confirm that @alexandernanberg/react-pdf-renderer@4.0.0-canary-2 is working with React 19. 🙌 🎉

@ZipBrandon
Copy link

I'm using it now too! Thanks so much @alexandernanberg!

@Timothylp
Copy link

Hello,

while trying with @alexandernanberg/react-pdf-renderer package, I can't get the PDF rendering to work, I still get the following error:

⨯ ..\node_modules\react-reconciler\cjs\react-reconciler.development.js (15700:59) @ module.exports
⨯ TypeError: Cannot read properties of undefined (reading 'S')
    at Results (./app/(frontend)/results/page.tsx:41:107)
digest: "3784830825"
  15698 |         _currentRenderer2: null
  15699 |       },
> 15700 |       prevOnStartTransitionFinish = ReactSharedInternals.S;
        |                                                           ^
  15701 |     ReactSharedInternals.S = function (transition, returnValue) {
  15702 |       "object" === typeof returnValue &&
  15703 |         null !== returnValue &&

Is it maybe because there was an update of the beta version of react 19?

@mamlzy
Copy link

mamlzy commented Oct 23, 2024

@alexandernanberg can you fix this bugs in your forked repo? https://github.com/diegomura/react-pdf/pull/2878/files

@alexandernanberg
Copy link
Contributor

I've rebased on main and released a new 4.0.0-canary-3 version of @alexandernanberg/react-pdf-renderer. Haven't had time to test much yet so let me know if it's working as expected

@alexandernanberg
Copy link
Contributor

I'm able to reproduce the issue outside my actual app https://github.com/alexandernanberg/react-pdf-renderer-nextjs , I'll look into it whenever I get time. Can't make any promises.

I can't share the whole lockfile but I'm using pnpm and I've locked the React versions to these + nextjs@15.0.1

catalogs:
  react19:
    react: 19.0.0-rc-69d4b800-20241021
    react-dom: 19.0.0-rc-69d4b800-20241021
    '@types/react': npm:types-react@rc
    '@types/react-dom': npm:types-react-dom@rc

Tried changing to those in my repro but still got the same issue. We might have to inline to react-reconciler into the renderer package instead of using it as a dep

@sam3d
Copy link

sam3d commented Nov 1, 2024

AFAICT react-reconciler is already inlined into the render package? The entire source of react-reconciler.production.js is present in the resulting outputs of react-pdf-renderer/lib

@alexandernanberg
Copy link
Contributor

You're right, had a brief experiment keeping it as a dep but removed that later 7c24f14.

So digging into this a bit more, React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE is undefined which the reconciler depends on.

There is however React.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, so looks like the React pkg adapts based on the env 🤔

@diegopluna
Copy link

Rendering server side. here is my code.

import type { NextRequest } from "next/server";

import { PdfExample, renderToStream } from "@repo/reports";

export async function GET(req: NextRequest) {
    const requestUrl = new URL(req.url);
    const stream = await renderToStream(
      await PdfExample(),
    );
    const blob = await new Response(stream).blob();

    const headers: Record<string, string> = {
      "Content-Type": "application/pdf",
      "Cache-Control": "no-store, max-age=0",
    };
    if (!preview) {
      headers["Content-Disposition"] =
        `attachment; filename="filename.pdf"`;
    }
    return new Response(blob, { headers });
}

renderToStream is exported from @alexandernanberg/react-pdf-renderer

I'm getting this error

ypeError: Cannot read properties of undefined (reading 'S')
    at module.exports (//.next/server/chunks/08b5e_@alexandernanberg_react-pdf-renderer_lib_react-pdf_1362ba.js:14494:67)
    at createRenderer /.next/server/chunks/08b5e_@alexandernanberg_react-pdf-renderer_lib_react-pdf_1362ba.js:14980:12)
    at pdf (/.next/server/chunks/08b5e_@alexandernanberg_react-pdf-renderer_lib_react-pdf_1362ba.js:15180:28)
    at renderToStream (/.next/server/chunks/08b5e_@alexandernanberg_react-pdf-renderer_lib_react-pdf_1362ba.js:15311:22)
    at GET (/.next/server/chunks/[root of the server]__03a291._.js:10336:252)
    at async AppRouteRouteModule.do (//next/dist/compiled/next-server/app-route.runtime.dev.js:10:32801)
    at async AppRouteRouteModule.handle (/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js:10:38302)

How are you not getting the:

Argument of type 'ReadableStream' is not assignable to parameter of type 'BodyInit | null | undefined'.ts(2345)

Typescript error on the blob definition?

@jpainam
Copy link

jpainam commented Nov 5, 2024

Rendering server side. here is my code.

import type { NextRequest } from "next/server";

import { PdfExample, renderToStream } from "@repo/reports";

export async function GET(req: NextRequest) {
    const requestUrl = new URL(req.url);
    const stream = await renderToStream(
      await PdfExample(),
    );
    const blob = await new Response(stream).blob();

    const headers: Record<string, string> = {
      "Content-Type": "application/pdf",
      "Cache-Control": "no-store, max-age=0",
    };
    if (!preview) {
      headers["Content-Disposition"] =
        `attachment; filename="filename.pdf"`;
    }
    return new Response(blob, { headers });
}

renderToStream is exported from @alexandernanberg/react-pdf-renderer
I'm getting this error

ypeError: Cannot read properties of undefined (reading 'S')
    at module.exports (//.next/server/chunks/08b5e_@alexandernanberg_react-pdf-renderer_lib_react-pdf_1362ba.js:14494:67)
    at createRenderer /.next/server/chunks/08b5e_@alexandernanberg_react-pdf-renderer_lib_react-pdf_1362ba.js:14980:12)
    at pdf (/.next/server/chunks/08b5e_@alexandernanberg_react-pdf-renderer_lib_react-pdf_1362ba.js:15180:28)
    at renderToStream (/.next/server/chunks/08b5e_@alexandernanberg_react-pdf-renderer_lib_react-pdf_1362ba.js:15311:22)
    at GET (/.next/server/chunks/[root of the server]__03a291._.js:10336:252)
    at async AppRouteRouteModule.do (//next/dist/compiled/next-server/app-route.runtime.dev.js:10:32801)
    at async AppRouteRouteModule.handle (/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js:10:38302)

How are you not getting the:

Argument of type 'ReadableStream' is not assignable to parameter of type 'BodyInit | null | undefined'.ts(2345)

Typescript error on the blob definition?

It works at runtime. just add // @ts-expect-error TODO: WTF Typescript right before const blob = await new Response(stream).blob();

This is due to TypeScript not recognizing ReadableStream as a valid arg to Response. I really don't know why.

BTW, it also works if you stream directly.

@diegopluna read https://midday.ai/updates/invoice-pdf
This was working fine before react 19 and nextjs 15

@diegopluna
Copy link

Rendering server side. here is my code.

import type { NextRequest } from "next/server";

import { PdfExample, renderToStream } from "@repo/reports";

export async function GET(req: NextRequest) {
    const requestUrl = new URL(req.url);
    const stream = await renderToStream(
      await PdfExample(),
    );
    const blob = await new Response(stream).blob();

    const headers: Record<string, string> = {
      "Content-Type": "application/pdf",
      "Cache-Control": "no-store, max-age=0",
    };
    if (!preview) {
      headers["Content-Disposition"] =
        `attachment; filename="filename.pdf"`;
    }
    return new Response(blob, { headers });
}

renderToStream is exported from @alexandernanberg/react-pdf-renderer
I'm getting this error

ypeError: Cannot read properties of undefined (reading 'S')
    at module.exports (//.next/server/chunks/08b5e_@alexandernanberg_react-pdf-renderer_lib_react-pdf_1362ba.js:14494:67)
    at createRenderer /.next/server/chunks/08b5e_@alexandernanberg_react-pdf-renderer_lib_react-pdf_1362ba.js:14980:12)
    at pdf (/.next/server/chunks/08b5e_@alexandernanberg_react-pdf-renderer_lib_react-pdf_1362ba.js:15180:28)
    at renderToStream (/.next/server/chunks/08b5e_@alexandernanberg_react-pdf-renderer_lib_react-pdf_1362ba.js:15311:22)
    at GET (/.next/server/chunks/[root of the server]__03a291._.js:10336:252)
    at async AppRouteRouteModule.do (//next/dist/compiled/next-server/app-route.runtime.dev.js:10:32801)
    at async AppRouteRouteModule.handle (/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js:10:38302)

How are you not getting the:

Argument of type 'ReadableStream' is not assignable to parameter of type 'BodyInit | null | undefined'.ts(2345)

Typescript error on the blob definition?

It works at runtime. just add // @ts-expect-error TODO: WTF Typescript right before const blob = await new Response(stream).blob();

This is due to TypeScript not recognizing ReadableStream as a valid arg to Response. I really don't know why.

BTW, it also works if you stream directly.

@diegopluna read https://midday.ai/updates/invoice-pdf This was working fine before react 19 and nextjs 15

Thanks, probabily will downgrade my project to next 14 while this isnt resolved!

@eps1lon
Copy link

eps1lon commented Nov 6, 2024

For people using @alexandernanberg/react-pdf-renderer:
Is it only breaking in Next.js route handlers and React Server components or is it also breaking in Client components. Would be helpful to verify if adding use client at the top helps. This doesn't work for Next.js Route handlers but pages can leverage use client.

@alexandernanberg
Copy link
Contributor

@eps1lon AFAICT it's breaking only in Server component and route handlers. E.g. this is working https://github.com/alexandernanberg/react-pdf-renderer-nextjs/blob/main/src/app/page.tsx as expected.

Any plans on supporting use client for route handlers, or other ways to opt out of Server components?

@eps1lon
Copy link

eps1lon commented Nov 7, 2024

How does this work for a page? Isn't the server use case to just save it as a file i.e. only relevant for API endpoints?

There's currently no plan to support Client components in Next.js Route handlers. You can still use pages/api though in Next.js which does support Client components (but no React Server Components).

@alexandernanberg
Copy link
Contributor

I think a common use case is to have a route handler generate the PDF and then use https://github.com/wojtekmaj/react-pdf to display it (by just giving it the endpoint URL). That way you don't need to handle the display and download cases differently, it's just a single endpoint that covers everything.

Got it. Haven't really looked into it, but does react-reconciler work in Server components? Just thinking if it's worth investigating if we could add Server component support. I suspect the majority of users are using Route handlers atm

@dmarcucci
Copy link

I think a common use case is to have a route handler generate the PDF and then use https://github.com/wojtekmaj/react-pdf to display it (by just giving it the endpoint URL).

I'm doing something a little different, but similar. I use an API to generate the PDF and then render it to a file in Azure Blob Storage.

Unfortunately, I cannot upgrade to Next 15 because of this issue.

@diegomura
Copy link
Owner

Hi all 👋🏻 Thanks for the discussion here, was very insightful. Thanks @alexandernanberg for #2783 and @eps1lon for your expertise here. I apologize for my absence, I do not count with much free time lately.

I had this issue in my radar, but to be honest I'm not sure yet what's the fix. While #2783 might work for React 19, my understanding is that it drops support for all other supported React versions (16, 17 and 18). While I'm fine on stop supporting 16, I think it's a no-go to just make this lib work for the latest React version.

The first thing I need to do is have a way to reproduce this. Basically have at least playgrounds for Next 14 (React 18) and Next 15 (React 19) so I can empirically test both scenarios (ideally then being able to add tests for it). I'll have this ready soon.

For the fix, I have a couple of ideas:

  1. Check if I can bump react-reconciler to 0.31 and still make old React versions to work. Based on @alexandernanberg PR (where previous versions are disabled) and old experiments I don't think it's possible, but still worths checking
  2. As stated above, react-pdf is currently bundling react-reconciler on it. To be honest I don't exactly remember the reasons behind this, but we could always bundle valid versions for React 18 and 19, and conditionally use one or the other internally based on React version. Don't love it, as it will add like 30kb to the gzipped bundle, but is an option
  3. Stop bundling react-reconciler and force user install a specific version

Any other idea or something I might be missing? 🙏🏻

@diegomura
Copy link
Owner

@eps1lon I noticed that the reconciler breaks when runs on route handlers or React Server Components because React.Component is undefined and it relies on it. Any idea why might be?

@joacub
Copy link
Author

joacub commented Nov 12, 2024

Hi all 👋🏻 Thanks for the discussion here, was very insightful. Thanks @alexandernanberg for #2783 and @eps1lon for your expertise here. I apologize for my absence, I do not count with much free time lately.

I had this issue in my radar, but to be honest I'm not sure yet what's the fix. While #2783 might work for React 19, my understanding is that it drops support for all other supported React versions (16, 17 and 18). While I'm fine on stop supporting 16, I think it's a no-go to just make this lib work for the latest React version.

The first thing I need to do is have a way to reproduce this. Basically have at least playgrounds for Next 14 (React 18) and Next 15 (React 19) so I can empirically test both scenarios (ideally then being able to add tests for it). I'll have this ready soon.

For the fix, I have a couple of ideas:

  1. Check if I can bump react-reconciler to 0.31 and still make old React versions to work. Based on @alexandernanberg PR (where previous versions are disabled) and old experiments I don't think it's possible, but still worths checking
  2. As stated above, react-pdf is currently bundling react-reconciler on it. To be honest I don't exactly remember the reasons behind this, but we could always bundle valid versions for React 18 and 19, and conditionally use one or the other internally based on React version. Don't love it, as it will add like 30kb to the gzipped bundle, but is an option
  3. Stop bundling react-reconciler and force user install a specific version

Any other idea or something I might be missing? 🙏🏻

3rd option would be perfect

@eps1lon
Copy link

eps1lon commented Nov 12, 2024

@eps1lon I noticed that the reconciler breaks when runs on route handlers or React Server Components because React.Component is undefined and it relies on it. Any idea why might be?

react-reconciler is only supported in the Client environment. Next.js Route handlers are in the Server environment so they can't support react-pdf in this form.

@diegomura
Copy link
Owner

diegomura commented Nov 13, 2024

@joacub I wouldn't call it perfect 😄 It's bad ux forcing users to install 2 deps and having to check versions based on what React they use. That's also considering we can internally support all versions at the same time (seems possible)

@eps1lon since my post I kept wondering if we really need reconciler in node. The major advantage of it is managing element lifecycle on a dynamic env like the client, but in node people generate static documents. So basically what we need is traversing the passed element tree to transform it into react-pdf internal document model (which is almost a 1-1 map between react elements and js objects). Biggest complexity I think are composite react elements (function and class components). Is there an existing tool you know besides the reconciler to achieve this? Am I minimizing how complex that would be? A bit unrelated to React 19 support though 😄

@alexandernanberg
Copy link
Contributor

IMO it's fine to drop support for older React versions in the latest release. I mean you keep supporting the v4 branch in parallel, will be some extra work but I suspect it will be less than supporting both reconcilers. This is what e.g. react-three-fiber will do, their v9 will require React 19 https://github.com/pmndrs/react-three-fiber/blob/v9/packages/fiber/package.json#L63-L64

@diegomura
Copy link
Owner

Thanks for the response @alexandernanberg !

IMO it's fine to drop support for older React versions in the latest release. I mean you keep supporting the v4 branch in parallel, will be some extra work but I suspect it will be less than supporting both reconcilers.

Unless I'm missing some data here, I don't really see how it would be simpler. I feel branching out for React 19 support makes it harder in lots of ways:

  • As a maintainer keeping 2 official releases in different branches is painful. We did that in the past for other cases. We have changesets integrated here from master and that would break (maybe there's a way to configure it in such way?). I'm sure there's automation that can be build. It just feels like a lot of work
  • As a user the simplicity of not having to worry about what version to install is unmatched, when possible of course. I feel in this case it is with some minor drawbacks. In general I don't like to make users pay the price of something that can be fixed in development.
  • From a semver perspective, such approach would mean react-pdf for older React versions will be locked and can't go through any major changes, as it would conflict with React 19 one. React 19 does not even have an official release in npm. While I think it's important to provide support, the main version until React 19 is fully adopted should be the current one. Branching that out feels odd in that sense :)

I'd love to hear thoughts about this :) I'd be happy to change my mind

Solution

we could always bundle valid versions for React 18 and 19, and conditionally use one or the other internally based on React version.

I'm currently navigating through this solution and I think overall is the one that has the best tradeoffs

Pros:

  • No need to drop support for older versions
  • No need to branch out react-pdf
  • Works out of the box for everyone, no need to be specific with versions
  • Docs and lib usage does not change
  • Seamless transition (and temporal) as we deprecate React version support over time. Once React 19 is the de-facto version, we go back to use one renderer version

Cons:

  • Harder to implement (happy to pay that price)
  • Bundle size: in order to conditionally toggle between 2 reconcilers on runtime, both versions need to be preset, adding an extra gzipped ~30kb to the bundle

I feel the latter is the most concerning point, and for that I thought about 2 things:

  • I experimented yesterday about trimming some things of the reconciler we don't need and shipping that version with react-pdf. We use it with very specific flags and just use a subset of the methods it exposes. By doing sone build-time transformations we can trim some of its code while keeping it work in the exact same way. I was able to remove around 15% of the code with some simple heuristics
  • Provide users a way to tree-shake it for further optimization

Sorry for the long post here. @alexandernanberg @eps1lon I'm very curious to hear your thoughts about this solution. Not sure it has been done before and I might be missing something

@sam3d
Copy link

sam3d commented Nov 15, 2024

Could the preact reconciler be used instead? I doubt it'll be as simple as a drop-in replacement but it seems to support both standard react and preact in <1KB

https://github.com/CodyJasonBennett/preact-reconciler

@diegomura
Copy link
Owner

Interesting idea @sam3d . It's crazy that package is less than 1kb gzipped. React one is like ~25kb. Not sure will work, but feels a bit scary to move away from react-reconciler, given these libs aren't very documented. In case anything goes wrong it's very hard to troubleshoot. But will take a look!

@eps1lon
Copy link

eps1lon commented Nov 15, 2024

No guarantees about compat with preact-reconciler. This may break in any React release.

You can also export a dedicated version for React Server components by adding an entrypoint to exports['react-server'] to your package.json that doesn't use react-reconciler. Client Components would still use the version using react-reconciler then.

@diegomura
Copy link
Owner

Thanks @eps1lon . I assume that was in response of "Is there an existing tool you know besides the reconciler to achieve this?"?

@diegomura
Copy link
Owner

Hey! I ended up implementing the solution I proposed here and from my tests it works very well. I think there's a bit more that can be done to reduce bundle size, but I don't expect a huge impact on the published version🤞🏻 . One day I hope I can remove this approach. Thanks for all the help here and please report anything you might find using it. I'll re-open this issue depending on what it is or create a new one if something specific does not work

@karlhorky
Copy link
Contributor

@diegomura and @alexandernanberg thanks for the React 19 compatibility implementation!

We upgraded to @react-pdf/renderer@4.1.2 in our project (which contains React 19) and can confirm that our visual regression tests on the PDFs are passing 🎉

@diegomura
Copy link
Owner

That's great news @karlhorky !! Thanks for letting us know

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.