resend / react-email

💌 Build and send emails using React
https://react.email
MIT License
12.67k stars 583 forks source link

Tailwind not parsing the rendered children from components #1021

Closed lunoob closed 6 months ago

lunoob commented 6 months ago

Describe the Bug

Before version 0.0.8, the following code is working normally

<Tailwind>
  <Template />
</Tailwind>

After version 0.0.8, I found that it only analyzes Component with the Children attribute

// it work fine
<Tailwind>
   <Component className="text-[100px]">React Email</Component>
</Tailwind>

// it not work for <div>
<Tailwind>
   <Component className="text-[100px]">
        <div className="text-red">Color</div>
        React Email
   </Component>
</Tailwind>

Which package is affected (leave empty if unsure)

@react-email/tailwind

Link to the code that reproduces this issue

example in codesandbox

To Reproduce

I compared the latest version and the source code of version 0.0.8, and found that the 0.0.8 version was replaced by the entire HTML, while the latest version only analyzes Children after the version 0.0.8.

0.0.8 image

0.0.8+ image

Expected Behavior

it can be parse the whole template component

What's your node version? (if relevant)

No response

gabrielmfern commented 6 months ago

Can you maybe elaborate more on what is happening to your email template? Does it work under some circumstances and others not, and if so, can you please tell me what circumstances?

giovannetti-eric commented 6 months ago

Same problem here, with @react-email/components": "0.0.7" my email templates were working well, after upgrade, a lot of Tailwind classes are not converted into style attributes anymore.

For example:

  <Link href={t("common.appStore.href")} className="inline-block">
    <Img
      src={t("common.appStore.img")}
      width="119"
      height="40"
      alt={t("common.appStore.label")}
      className="max-w-full h-auto inline"
    />
  </Link>

is generating

<a href="xxx" class="inline-block" style="color:#067df7;text-decoration:none" target="_blank"><img class="max-w-full h-auto inline" alt="xxx" height="40" src="xxx" style="display:block;outline:none;border:none;text-decoration:none" width="119"></a>

After some test, I found that the problem apply when I load custom components in an email template, for example:

import * as React from "react";
import MainLayout from "../components/ui/MainLayout";
import Email1 from "../components/views/Email1";
import { useGetToken } from "../hooks/useGetToken";

const locale = "en";

export const Email1En = () => {
  const { t } = useGetToken(locale);

  return (
    <MainLayout
      title={t("pnp23bf1_22.title")}
      preview={t("pnp23bf1_22.preview")}
    >
      <Email1 locale={locale} />
    </MainLayout>
  );
};

export default Email1En;
import { Body, Head, Html, Preview } from "@react-email/components";
import React from "react";
import TailwindProvider from "./TailwindProvider";

interface MainLayoutProps {
  title?: string;
  preview?: string;
  children?: React.ReactNode;
}

export const MainLayout = ({ title, preview, children }: MainLayoutProps) => {
  return (
      <Html>
        <Head>
          <title>{title}</title>
          <link rel="preconnect" href="https://fonts.googleapis.com" />
          <link
            rel="preconnect"
            href="https://fonts.gstatic.com"
            crossOrigin="true"
          />
          <link
            href="https://fonts.googleapis.com/css2?family=Roboto+Slab&display=swap"
            rel="stylesheet"
          />
          <style>
            {`
              @media screen and (max-width: 600px) {
                h1 {
                  font-size: 30px !important;
                }
              }
            `}
          </style>
        </Head>
        {preview && <Preview>{preview}</Preview>}
        <TailwindProvider>
          <Body className="text-gray-900 bg-white my-auto mx-auto font-sans leading-snug">
              {children}
          </Body>
        </TailwindProvider>
      </Html>
  );
};

export default MainLayout;

Most Tailwind classes into <Email1 /> are not generated

lunoob commented 6 months ago

Can you maybe elaborate more on what is happening to your email template? Does it work under some circumstances and others not, and if so, can you please tell me what circumstances?

yes, I maked an example

gabrielmfern commented 6 months ago

Going to take a look into fixing this to unblock you guys, a PR is coming that is going to improve a lot of things about the Tailwind component.

Maclay74 commented 6 months ago

I think it might be related with recent react update. Essentially, they "fixed bug" when rendering functions take context into account.

gabrielmfern commented 6 months ago

Same problem here, with @react-email/components": "0.0.7" my email templates were working well, after upgrade, a lot of Tailwind classes are not converted into style attributes anymore.

For example:

  <Link href={t("common.appStore.href")} className="inline-block">
    <Img
      src={t("common.appStore.img")}
      width="119"
      height="40"
      alt={t("common.appStore.label")}
      className="max-w-full h-auto inline"
    />
  </Link>

is generating

<a href="xxx" class="inline-block" style="color:#067df7;text-decoration:none" target="_blank"><img class="max-w-full h-auto inline" alt="xxx" height="40" src="xxx" style="display:block;outline:none;border:none;text-decoration:none" width="119"></a>

After some test, I found that the problem apply when I load custom components in an email template, for example:

import * as React from "react";
import MainLayout from "../components/ui/MainLayout";
import Email1 from "../components/views/Email1";
import { useGetToken } from "../hooks/useGetToken";

const locale = "en";

export const Email1En = () => {
  const { t } = useGetToken(locale);

  return (
    <MainLayout
      title={t("pnp23bf1_22.title")}
      preview={t("pnp23bf1_22.preview")}
    >
      <Email1 locale={locale} />
    </MainLayout>
  );
};

export default Email1En;
import { Body, Head, Html, Preview } from "@react-email/components";
import React from "react";
import TailwindProvider from "./TailwindProvider";

interface MainLayoutProps {
  title?: string;
  preview?: string;
  children?: React.ReactNode;
}

export const MainLayout = ({ title, preview, children }: MainLayoutProps) => {
  return (
      <Html>
        <Head>
          <title>{title}</title>
          <link rel="preconnect" href="https://fonts.googleapis.com" />
          <link
            rel="preconnect"
            href="https://fonts.gstatic.com"
            crossOrigin="true"
          />
          <link
            href="https://fonts.googleapis.com/css2?family=Roboto+Slab&display=swap"
            rel="stylesheet"
          />
          <style>
            {`
              @media screen and (max-width: 600px) {
                h1 {
                  font-size: 30px !important;
                }
              }
            `}
          </style>
        </Head>
        {preview && <Preview>{preview}</Preview>}
        <TailwindProvider>
          <Body className="text-gray-900 bg-white my-auto mx-auto font-sans leading-snug">
              {children}
          </Body>
        </TailwindProvider>
      </Html>
  );
};

export default MainLayout;

Most Tailwind classes into <Email1 /> are not generated

Working on a fix for this on a new PR that also improves performance.

The problem here is that only the children passed through props to components are handled, while the resulting children of the rendered component are not.

This is indeed a bug and we are on it.

gabrielmfern commented 6 months ago

PR that solves this is basically done, this problem will go away soon guys.

If it takes too long for us to merge and release a new version out, I made a quick patch that fixes the issue as a workaround

diff --git a/dist/index.js b/dist/index.js
index 12a4d02aac2b2364deb79d7785bb5b2d735d9e12..12a042dd383a3e91cbfe803af11210c5b40457a7 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -154,6 +154,17 @@ function processElement(element, headStyles, twi) {
       ...processedChildren
     );
   }
+  if (typeof modifiedElement.type === "function") {
+    const component = modifiedElement.type;
+    const renderedComponent = component(modifiedElement.props);
+    if (React.isValidElement(renderedComponent)) {
+      modifiedElement = processElement(
+        renderedComponent,
+        headStyles,
+        twi
+      );
+    }
+  }
   return modifiedElement;
 }
 function processHead(child, responsiveStyles) {
diff --git a/dist/index.mjs b/dist/index.mjs
index 2e58d8425e6bf5ed3928e99d20fd1e1006d70e36..3f7b69f98aaf9ab4c45c27153d55b774e6e326f8 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -121,6 +121,17 @@ function processElement(element, headStyles, twi) {
       ...processedChildren
     );
   }
+  if (typeof modifiedElement.type === "function") {
+    const component = modifiedElement.type;
+    const renderedComponent = component(modifiedElement.props);
+    if (React.isValidElement(renderedComponent)) {
+      modifiedElement = processElement(
+        renderedComponent,
+        headStyles,
+        twi
+      );
+    }
+  }
   return modifiedElement;
 }
 function processHead(child, responsiveStyles) {
J4v4Scr1pt commented 6 months ago

Still not working for me. But my workaround with this is to wrap the component in Tailwind as well.. maybe not the best.. but it works for now.. 😅

import { Img, Section, Tailwind } from '@react-email/components';
import * as React from 'react';
import tailwindConfig from '../../tailwind.config';

export const Header = () => (
    <Tailwind config={tailwindConfig}>
        <Section className="round flex h-[120px] items-center justify-center rounded-tl-lg rounded-tr-lg bg-wt-black">
            <Img
                src="src"
            />
        </Section>
    </Tailwind>
);
gabrielmfern commented 6 months ago

Still not working for me. But my workaround with this is to wrap the component in Tailwind as well.. maybe not the best.. but it works for now.. 😅

import { Img, Section, Tailwind } from '@react-email/components';
import * as React from 'react';
import tailwindConfig from '../../tailwind.config';

export const Header = () => (
  <Tailwind config={tailwindConfig}>
      <Section className="round flex h-[120px] items-center justify-center rounded-tl-lg rounded-tr-lg bg-wt-black">
          <Img
              src="src"
          />
      </Section>
  </Tailwind>
);

can you maybe give a reproduction so I can narrow down the problem on your case?

J4v4Scr1pt commented 6 months ago

In my case its just that when I import this in to my email-template the tailwind styles dont get applied to the component.

import {
    Body,
    Button,
    Column,
    Container,
    Head,
    Heading,
    Hr,
    Html,
    Img,
    Link,
    Preview,
    Row,
    Section,
    Tailwind,
    Text,
} from '@react-email/components';
import * as React from 'react';
import tailwindConfig from '../tailwind.config';
import { Header } from './components/Header';
import { Footer } from './components/Footer';

export const AssignedRole = () => {
    return (
        <Html>
            <Head />
            <Preview>{`Preview text`}</Preview>
            <Tailwind config={tailwindConfig}>
                <Body className="mx-auto my-auto rounded-lg bg-white font-sans">
                    <Container className="mx-auto my-[40px] w-[600px] rounded-lg border border-solid border-[#eaeaea]"> // Styles work
                        <Header /> // No styles without wrapping the component in Tailwind
                    // ALLL THE CONTENT and all styles work
                        <Footer /> // No styles without wrapping the component in Tailwind
                    </Container>
                </Body>
            </Tailwind>
        </Html>
    );
};

export default AssignedRole;
gabrielmfern commented 6 months ago

In my case its just that when I import this in to my email-template the tailwind styles dont get applied to the component.

import {
  Body,
  Button,
  Column,
  Container,
  Head,
  Heading,
  Hr,
  Html,
  Img,
  Link,
  Preview,
  Row,
  Section,
  Tailwind,
  Text,
} from '@react-email/components';
import * as React from 'react';
import tailwindConfig from '../tailwind.config';
import { Header } from './components/Header';
import { Footer } from './components/Footer';

export const AssignedRole = () => {
  return (
      <Html>
          <Head />
          <Preview>{`Preview text`}</Preview>
          <Tailwind config={tailwindConfig}>
              <Body className="mx-auto my-auto rounded-lg bg-white font-sans">
                  <Container className="mx-auto my-[40px] w-[600px] rounded-lg border border-solid border-[#eaeaea]"> // Styles work
                      <Header /> // No styles without wrapping the component in Tailwind
                  // ALLL THE CONTENT and all styles work
                      <Footer /> // No styles without wrapping the component in Tailwind
                  </Container>
              </Body>
          </Tailwind>
      </Html>
  );
};

export default AssignedRole;

Could you try importing from @react-email/tailwind instead of components?

J4v4Scr1pt commented 6 months ago

I installed the Tailwind component and used it directly instead as you suggested but no styles get applied 🙁

gabrielmfern commented 6 months ago

I installed the Tailwind component and used it directly instead as you suggested but no styles get applied 🙁

how did you install the patch?

J4v4Scr1pt commented 6 months ago

Ahh maybe that the issue. I just installed it like this: pnpm add @react-email/tailwind -E

"prettier-plugin-tailwindcss": "^0.5.7",

gabrielmfern commented 6 months ago

Ahh maybe that the issue. I just installed it like this: pnpm add @react-email/tailwind -E

"prettier-plugin-tailwindcss": "^0.5.7",

As mentioned more above in the issue, we don't yet have a release with the issue fixed, but it is coming, so you'll have to use the patch for now. To use the patch you can either use pnpm's built-in patching mechanism, or use something like patch-package.

If you prefer using pnpm, you will need to go through the following:

  1. add the following to the end of your package.json
    "pnpm": {
    "patchedDependencies": {
      "@react-email/tailwind@0.0.12": "patches/@react-email__tailwind@0.0.12.patch"
    }
    }
  2. copy and paste the following inside of patches/@react-email__tailwind@0.0.12.patch
    diff --git a/dist/index.js b/dist/index.js
    index 12a4d02aac2b2364deb79d7785bb5b2d735d9e12..12a042dd383a3e91cbfe803af11210c5b40457a7 100644
    --- a/dist/index.js
    +++ b/dist/index.js
    @@ -154,6 +154,17 @@ function processElement(element, headStyles, twi) {
       ...processedChildren
     );
    }
    +  if (typeof modifiedElement.type === "function") {
    +    const component = modifiedElement.type;
    +    const renderedComponent = component(modifiedElement.props);
    +    if (React.isValidElement(renderedComponent)) {
    +      modifiedElement = processElement(
    +        renderedComponent,
    +        headStyles,
    +        twi
    +      );
    +    }
    +  }
    return modifiedElement;
    }
    function processHead(child, responsiveStyles) {
    diff --git a/dist/index.mjs b/dist/index.mjs
    index 2e58d8425e6bf5ed3928e99d20fd1e1006d70e36..3f7b69f98aaf9ab4c45c27153d55b774e6e326f8 100644
    --- a/dist/index.mjs
    +++ b/dist/index.mjs
    @@ -121,6 +121,17 @@ function processElement(element, headStyles, twi) {
       ...processedChildren
     );
    }
    +  if (typeof modifiedElement.type === "function") {
    +    const component = modifiedElement.type;
    +    const renderedComponent = component(modifiedElement.props);
    +    if (React.isValidElement(renderedComponent)) {
    +      modifiedElement = processElement(
    +        renderedComponent,
    +        headStyles,
    +        twi
    +      );
    +    }
    +  }
    return modifiedElement;
    }
    function processHead(child, responsiveStyles) {
  3. Run pnpm install and the patch should be applied and the component should work now. Something to note that is important is that if you are not using pnpm somewhere the patch won't be applied, in a CI pipeline for example, so be sure to look out for that. A plus to patch-package if you are just using normal npm.

Also make sure you have @react-email/tailwind@0.0.12 installed since the patch is made for this version. For the patch-package way, you can look into its docs and I believe its going to be pretty similar to the above.


For context, a patch would be a manual change to a installed module that has some kind of problem that has not been fixed on a new version, its a sad reality, but sometimes we need patches on certain modules.

So instead of needing to do it manually there are these tools (such as pnpm patch or patch-package) that help you do this easily and in a way that everyone that also installs the dependencies of the project does not have to go through the library code again or apply the change manually.

Let me know if this works for you

OussamaFadlaoui commented 6 months ago

Ahh maybe that the issue. I just installed it like this: pnpm add @react-email/tailwind -E "prettier-plugin-tailwindcss": "^0.5.7",

As mentioned more above in the issue, we don't yet have a release with the issue fixed, but it is coming, so you'll have to use the patch for now. To use the patch you can either use pnpm's built-in patching mechanism, or use something like patch-package.

If you prefer using pnpm, you will need to go through the following:

  1. add the following to the end of your package.json
  "pnpm": {
    "patchedDependencies": {
      "@react-email/tailwind@0.0.12": "patches/@react-email__tailwind@0.0.12.patch"
    }
  }
  1. copy and paste the following inside of patches/@react-email__tailwind@0.0.12.patch
diff --git a/dist/index.js b/dist/index.js
index 12a4d02aac2b2364deb79d7785bb5b2d735d9e12..12a042dd383a3e91cbfe803af11210c5b40457a7 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -154,6 +154,17 @@ function processElement(element, headStyles, twi) {
       ...processedChildren
     );
   }
+  if (typeof modifiedElement.type === "function") {
+    const component = modifiedElement.type;
+    const renderedComponent = component(modifiedElement.props);
+    if (React.isValidElement(renderedComponent)) {
+      modifiedElement = processElement(
+        renderedComponent,
+        headStyles,
+        twi
+      );
+    }
+  }
   return modifiedElement;
 }
 function processHead(child, responsiveStyles) {
diff --git a/dist/index.mjs b/dist/index.mjs
index 2e58d8425e6bf5ed3928e99d20fd1e1006d70e36..3f7b69f98aaf9ab4c45c27153d55b774e6e326f8 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -121,6 +121,17 @@ function processElement(element, headStyles, twi) {
       ...processedChildren
     );
   }
+  if (typeof modifiedElement.type === "function") {
+    const component = modifiedElement.type;
+    const renderedComponent = component(modifiedElement.props);
+    if (React.isValidElement(renderedComponent)) {
+      modifiedElement = processElement(
+        renderedComponent,
+        headStyles,
+        twi
+      );
+    }
+  }
   return modifiedElement;
 }
 function processHead(child, responsiveStyles) {
  1. Run pnpm install and the patch should be applied and the component should work now. Something to note that is important is that if you are not using pnpm somewhere the patch won't be applied, in a CI pipeline for example, so be sure to look out for that. A plus to patch-package if you are just using normal npm.

Also make sure you have @react-email/tailwind@0.0.12 installed since the patch is made for this version. For the patch-package way, you can look into its docs and I believe its going to be pretty similar to the above.

For context, a patch would be a manual change to a installed module that has some kind of problem that has not been fixed on a new version, its a sad reality, but sometimes we need patches on certain modules.

So instead of needing to do it manually there are these tools (such as pnpm patch or patch-package) that help you do this easily and in a way that everyone that also installs the dependencies of the project does not have to go through the library code again or apply the change manually.

Let me know if this works for you

Appreciate this temporary alternative. It's not perfect but a better workaround than wrapping multiple components around <Tailwind />

gabrielmfern commented 6 months ago

Appreciate this temporary alternative. It's not perfect but a better workaround than wrapping multiple components around <Tailwind />

Hey @OussamaFadlaoui we have a new canary with a PR that should've fixed these kinds of issues, try out @react-email/tailwind@0.0.13-canary.3 and any problems you have we will fix. Thanks for the patience!

tomaszczura commented 6 months ago

Unfortunately this still does not work for me event with @react-email/tailwind v0.0.13 - styles are not applied to custom components

EDIT: My components were wrapped with memo. I removed that and now it is working - styles are passed

lunoob commented 6 months ago

Unfortunately this still does not work for me event with @react-email/tailwind v0.0.13 - styles are not applied to custom components

me too

gabrielmfern commented 5 months ago

Hey guys @lunoob @tomaszczura, the original underlying problem here was fixed. If there is a new problem I recommend you open up a new issue because it is going to be something else that may be somewhat of related to the problem. Also, please don't forget to make a reproduction of your problem because that makes it much easier for me to help. Thanks!

chris-trait commented 5 months ago

Hey @gabrielmfern Is it not possible to use react hooks within the Tailwind component?

If I try using a React hook inside of my e-mail components wrapped in <Tailwind> then I get the rules of hooks error:

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.

Minimal reproduction looks like:

const TestA = () => {
    const [foo] = React.useState("hello")
    return <p>{foo}</p>
}
const html = render(
    <Tailwind>
        <TestA />
    </Tailwind>
)

The use case is to wrap a React context around the e-mail with common props and then consume them from some child components.

gabrielmfern commented 5 months ago

@chris-trait Hey, this is expected behavior, email templates will generally not work with hooks since they are supposed to be static, all data that you want to add anywhere on it you should be passing through props to it before rendering with @react-email/render. Hooks will work if you are rendering the email directly with React's rendering process but won't with our rendering process because it generates static markup.

chris-trait commented 5 months ago

@gabrielmfern that does seem a little weird to me, as I do expect hooks to run for other "static" use cases like static site generation or server-side rendering, especially as async server side rendering is becoming more of a thing, and indeed hooks run fine with renderToStaticMarkup. React contexts also work fine in static rendering, the issue is with how the tailwind function traverses the component tree and calls components as plain functions: https://github.com/resendlabs/react-email/blob/2374d0d099360c63fe7427febb239776c3bd6b3b/packages/tailwind/src/tailwind.tsx#L95C32-L95C32.

For now my workaround is using <MyReactContext.Consumer>{context => ...}</MyReactContext.Consumer> instead of the hook.

gabrielmfern commented 5 months ago

@gabrielmfern that does seem a little weird to me, as I do expect hooks to run for other "static" use cases like static site generation or server-side rendering, especially as async server side rendering is becoming more of a thing, and indeed hooks run fine with renderToStaticMarkup. React contexts also work fine in static rendering, the issue is with how the tailwind function traverses the component tree and calls components as plain functions: https://github.com/resendlabs/react-email/blob/2374d0d099360c63fe7427febb239776c3bd6b3b/packages/tailwind/src/tailwind.tsx#L95C32-L95C32.

For now my workaround is using <MyReactContext.Consumer>{context => ...}</MyReactContext.Consumer> instead of the hook.

That's interesting, then I think this is an actual issue with the new Tailwind version, can you open an issue on this so we can reference it on a PR and discuss it there?