shellscape / jsx-email

Build emails with a delightful DX
https://jsx.email
MIT License
897 stars 28 forks source link

Invalid Hook Call #108

Open mckelveygreg opened 6 months ago

mckelveygreg commented 6 months ago

Expected Behavior

To be able to use 3rd party libs that may have react hooks in them (ie useMemo for performance concerns in a browser)

The library that alerted me to this is, @portabletext/react and their component PortableText. This enables us to render rich text from our CMS into emails. This code was able to run in the original react-email, but I'm unsure of how the library has shifted in this respect since then.

Actual Behavior

Throws Invalid Hook Call errors during the jsxToString function call

Additional Information

stack trace

chunk-I7UPNA6J.js?v=83f9a7a2:3509 Uncaught TypeError: Cannot read properties of null (reading 'useMemo')
    at useMemo (chunk-I7UPNA6J.js?v=83f9a7a2:3509:29)
    at ThirdPartyUseMemo (Reproduction.tsx?t=1703115101276:2146:16)
    at jsxToString (jsx-email.js?v=bf6a5f6d:24773:26)
    at jsxToString (jsx-email.js?v=bf6a5f6d:24766:24)
    at jsxToString (jsx-email.js?v=bf6a5f6d:24773:14)
    at jsxToString (jsx-email.js?v=bf6a5f6d:24725:22)
    at async jsxToString (jsx-email.js?v=bf6a5f6d:24766:18)
    at async render (jsx-email.js?v=bf6a5f6d:25206:15)
    at async main.tsx:67:16
    at async Promise.all (jsxemailrepro9a6jzc-exbx--55420--a2aabdd9.local-credentialless.webcontainer.io/index 0)
useMemo @ chunk-I7UPNA6J.js?v=83f9a7a2:3509
ThirdPartyUseMemo @ Reproduction.tsx?t=1703115101276:2146
jsxToString @ jsx-email.js?v=bf6a5f6d:24773
jsxToString @ jsx-email.js?v=bf6a5f6d:24766
jsxToString @ jsx-email.js?v=bf6a5f6d:24773
jsxToString @ jsx-email.js?v=bf6a5f6d:24725
Show 5 more frames
Show less
shellscape commented 6 months ago

Generally speaking, hooks aren't supported (right now). That's because email templates aren't interactive, and hooks are only really useful in interactive/reactive components that require hydration. While you might have an interactive/reactive component re-use use-case, they're really not a good fit for email templates because at the end of the day, email template outputs are static.

Immediate options for getting you past this:

I'll take a look at what it would take to support hooks, but it's important to understand what jsx-email (and even react-email and mjml) are actually providing to you - it's not a react app, it's a static file template engine with sugar to make the task easy.

mckelveygreg commented 6 months ago

I understand this isn't a React app, and I don't expect state or anything, but this is a third party lib that turns rich text into DOM.

Looks like I should try to find an alternative method from that lib.

shellscape commented 6 months ago

This particular error seems to be common, with a common cause. Going to investigate a little bit on my end to see if our bundle may be to blame.

shellscape commented 6 months ago

So investigating this actually surfaced a few issues that will result in improvements, but none that are directly related to this issue unfortunately. There were dual versions of react within the bundled template (which is output to a temp directory); one in the bundled template, and one in node_modules for the project itself. That's resolved. The bundling of the email templates are actually faster now since it's excluding jsx-email and react. That took the bundled file size for a template from 7mb to 25kb. So that's fun. But again, won't resolve this issue.

This is probably related to react's internal dispatcher but not quite sure yet what the deal is there.

mckelveygreg commented 6 months ago

Whelp, found a work around for this! If I just render the react to html myself, then everyone is happy 🤷

import { PortableText } from "@portabletext/react"
import { renderToString } from "react-dom/server"

export const Template = () => {
  const html = renderToString(
    <PortableText
      value={body.filter(isTypedObject)}
      components={dynamicComponents}
      onMissingComponent={onMissingComponent}
    />
  )
return (
  <div dangerouslySetInnerHTML={{
                __html: html,
              }}
           />
 )
}

So I guess we could close? 🤷