emotion icon indicating copy to clipboard operation
emotion copied to clipboard

Plans to support Next.js 13 - /app directory

Open fcisio opened this issue 2 years ago • 168 comments

The problem

Next JS just release their v13 publicly. As seen in their docs, emotion has not yet added support.

Is there any plan to add support in the near future?

Thanks.

fcisio avatar Oct 25 '22 21:10 fcisio

It might be possible see about circling the wagons with the MUI and Vercel teams, as well, on this. MUI is very widely used (and teams are paying!). I am not sure what the specific numbers are, but I have to imagine we have a very large contingent of MUI/Emotion users overlapping with Next.js. Having these two titans not work together is a miss.

(I suspect if getting this working needed some sponsorship, $$$ could be found!)

adamrneary avatar Oct 26 '22 00:10 adamrneary

We’re also super looking for this so Theme UI can support the app directory!

lachlanjc avatar Oct 26 '22 00:10 lachlanjc

We may want to add an explicit API for this but this works today:

// app/emotion.tsx
"use client";
import { CacheProvider } from "@emotion/react";
import createCache from "@emotion/cache";
import { useServerInsertedHTML } from "next/navigation";
import { useState } from "react";

export default function RootStyleRegistry({
  children,
}: {
  children: JSX.Element;
}) {
  const [cache] = useState(() => {
    const cache = createCache({ key: "css" });
    cache.compat = true;
    return cache;
  });

  useServerInsertedHTML(() => {
    return (
      <style
        data-emotion={`${cache.key} ${Object.keys(cache.inserted).join(" ")}`}
        dangerouslySetInnerHTML={{
          __html: Object.values(cache.inserted).join(" "),
        }}
      />
    );
  });

  return <CacheProvider value={cache}>{children}</CacheProvider>;
}

// app/layout.tsx
import RootStyleRegistry from "./emotion";

export default function RootLayout({ children }: { children: JSX.Element }) {
  return (
    <html>
      <head></head>
      <body>
        <RootStyleRegistry>{children}</RootStyleRegistry>
      </body>
    </html>
  );
}

// app/page.tsx
/** @jsxImportSource @emotion/react */
"use client";

export default function Page() {
  return <div css={{ color: "green" }}>something</div>;
}

emmatown avatar Oct 27 '22 05:10 emmatown

@mitchellhamilton did you get this working for you?

Trying in a StackBlitz just now, it seems like it's giving me an error about React.createContext not being a function:

StackBlitz: https://stackblitz.com/edit/vercel-next-js-mxnxa7?file=app%2Fpage.tsx,app%2Flayout.tsx,app%2FEmotionRootStyleRegistry.tsx,next.config.js

event - compiled client and server successfully in 59 ms (403 modules)
error - (sc_server)/node_modules/@emotion/react/dist/emotion-element-b63ca7c6.cjs.dev.js (20:47) @ eval
error - TypeError: React.createContext is not a function
    at eval (webpack-internal:///(sc_server)/./node_modules/@emotion/react/dist/emotion-element-b63ca7c6.cjs.dev.js:19:49)
    at (sc_server)/./node_modules/@emotion/react/dist/emotion-element-b63ca7c6.cjs.dev.js (/home/projects/vercel-next-js-mxnxa7/.next/server/app/page.js:501:1)
    at __webpack_require__ (/home/projects/vercel-next-js-mxnxa7/.next/server/webpack-runtime.js:33:43)
    at eval (webpack-internal:///(sc_server)/./node_modules/@emotion/react/jsx-dev-runtime/dist/emotion-react-jsx-dev-runtime.cjs.dev.js:7:22)
    at (sc_server)/./node_modules/@emotion/react/jsx-dev-runtime/dist/emotion-react-jsx-dev-runtime.cjs.dev.js (/home/projects/vercel-next-js-mxnxa7/.next/server/app/page.js:512:1)
    at __webpack_require__ (/home/projects/vercel-next-js-mxnxa7/.next/server/webpack-runtime.js:33:43)
    at eval (webpack-internal:///(sc_server)/./app/layout.tsx:5:88)
    at (sc_server)/./app/layout.tsx (/home/projects/vercel-next-js-mxnxa7/.next/server/app/page.js:403:1)
    at __webpack_require__ (/home/projects/vercel-next-js-mxnxa7/.next/server/webpack-runtime.js:33:43)
    at Object.layout (webpack-internal:///(sc_server)/./node_modules/next/dist/build/webpack/loaders/next-app-loader.js?name=app%2Fpage&appPaths=%2Fpage&pagePath=private-next-app-dir%2Fpage.tsx&appDir=%2Fhome%2Fprojects%2Fvercel-next-js-mxnxa7%2Fapp&pageExtensions=tsx&pageExtensions=ts&pageExtensions=jsx&pageExtensions=js&rootDir=%2Fhome%2Fprojects%2Fvercel-next-js-mxnxa7&isDev=true&tsconfigPath=tsconfig.json!:22:99) {
  type: 'TypeError',
  page: '/'
}
null

Screenshot 2022-10-27 at 19 46 47


It does seem to work if the layout component is made into a client component, but this would be unfortunate:

StackBlitz: https://stackblitz.com/edit/vercel-next-js-tbkg4a?file=app%2Flayout.tsx,app%2Fpage.tsx,app%2FEmotionRootStyleRegistry.tsx,next.config.js

karlhorky avatar Oct 27 '22 17:10 karlhorky

Oh it seems like my optimization of removing the /** @jsxImportSource @emotion/react */ and using { compiler: { emotion: true } } (the SWC Emotion transform plugin) caused this to break (I guess this is still using context under the hood, will open an issue in Next.js repo).

Working StackBlitz, based on @emmatown's original example:

StackBlitz: https://stackblitz.com/edit/vercel-next-js-ajvkxp?file=app%2Fpage.tsx,app%2Flayout.tsx,app%2FEmotionRootStyleRegistry.tsx,next.config.js

karlhorky avatar Oct 27 '22 18:10 karlhorky

Reported a bug about the SWC Emotion transform plugin here:

  • https://github.com/vercel/next.js/issues/41994

karlhorky avatar Oct 27 '22 18:10 karlhorky

Just note that the presented solution works with the app directory - it still doesn't quite work with streaming. It's not exactly Emotion's fault though, other libraries won't work either because the callback provided to useServerInsertedHTML gets only called once. So it's only possible to inject styles contained in the initial "shell" this way.

You can observe this on this stackblitz that uses Styled Components. I've prepared it by copy-pasting the example from the Next.js docs, on top of that I've just added a single Suspense boundary to "chunk" the stream. The rendered span should have a red text but the whole thing stays green.

Andarist avatar Oct 27 '22 21:10 Andarist

Thank you all so much for the samples & explanation! @Andarist, could you give a little more color on the long-term situation here? If Next resolves the SWC bug & Emotion does an update, where will that leave us with server component support? Are there aspects that are never going to work?

lachlanjc avatar Oct 28 '22 05:10 lachlanjc

To the best of my understanding - we should be able to support server components in the future. Some parts of that are fuzzy to me though. Mainly I'm not sure how to avoid injecting the same styles back to the client on server component refetches. We rely on a list of inserted IDs but server components render for a particular request - where we don't have access to the IDs inserted by previous requests (or by the client, for that matter).

Andarist avatar Oct 28 '22 06:10 Andarist

I too need Mui + emotions to work, this would greatly speed my migration to client/server component architecture

murrowblue22 avatar Nov 01 '22 14:11 murrowblue22

As of right now, I converted all the components into client components to ‘migrate’ to nexjs13. 😂 Need this before any meaningful migration

songhobby avatar Nov 06 '22 18:11 songhobby

@mitchellhamilton Is cache.compat something exclusive to Emotion 10? When I run this setup on the latest version I get TypeError: Cannot read properties of undefined (reading 'registered') and TypeError: Cannot read properties of undefined (reading 'compat')

Rafcin avatar Nov 08 '22 18:11 Rafcin

To the best of my understanding - we should be able to support server components in the future. Some parts of that are fuzzy to me though. Mainly I'm not sure how to avoid injecting the same styles back to the client on server component refetches. We rely on a list of inserted IDs but server components render for a particular request - where we don't have access to the IDs inserted by previous requests (or by the client, for that matter).

Have you found any solutions yet? if you have can we kindly get a timeline for emotion-js working with nextjs13

godfrednanaowusu avatar Nov 17 '22 17:11 godfrednanaowusu

@godfrednanaowusu I ported a next 12 project to next 13 and have not had any trouble with emotion and mui working correctly (besides a breaking change in next/link). The issues here appear to be about using the /app directory instead of /pages. Since /app is listed as beta anyways, perhaps this isn't such a major obstacle to using next 13 with a plan to migrate project structure at a later date.

Essentially by asking to use the react 18 features with /app, this is asking emotion to fully support react 18 fully, which is going to involve some pretty big structural changes to do right.

mi-na-bot avatar Nov 18 '22 00:11 mi-na-bot

@minervabot read the title, the issue is about nextjs 13 app directory, not just upgrading and use pages folder

unfernandito avatar Nov 18 '22 01:11 unfernandito

@unfernandito I noticed a lot of people were talking about needing this to use 13, which seemed to perhaps deserve some clarification, since /app is not even in the stable next.js docs yet.

mi-na-bot avatar Nov 18 '22 01:11 mi-na-bot

After talking with the Next.js team and helping them recognize the problem with useServerInsertedHTML and Suspense that issue has been fixed in https://github.com/vercel/next.js/pull/42293

With that fix Emotion roughly works in the /app if you do this:

"use client";
import { CacheProvider } from "@emotion/react";
import createCache from "@emotion/cache";
import { useServerInsertedHTML } from "next/navigation";
import { useState } from "react";

export default function RootStyleRegistry({
  children,
}: {
  children: JSX.Element;
}) {
  const [{ cache, flush }] = useState(() => {
    const cache = createCache({ key: "my" });
    cache.compat = true;
    const prevInsert = cache.insert;
    let inserted: string[] = [];
    cache.insert = (...args) => {
      const serialized = args[1];
      if (cache.inserted[serialized.name] === undefined) {
        inserted.push(serialized.name);
      }
      return prevInsert(...args);
    };
    const flush = () => {
      const prevInserted = inserted;
      inserted = [];
      return prevInserted;
    };
    return { cache, flush };
  });

  useServerInsertedHTML(() => {
    const names = flush();
    if (names.length === 0) return null;
    let styles = "";
    for (const name of names) {
      styles += cache.inserted[name];
    }
    return (
      <style
        data-emotion={`${cache.key} ${names.join(" ")}`}
        dangerouslySetInnerHTML={{
          __html: styles,
        }}
      />
    );
  });

  return <CacheProvider value={cache}>{children}</CacheProvider>;
}

(kudos to the @emmatown for providing this implementation)

We need to figure out the exact APIs for handling this within Emotion but conceptually the thing would end up being very similar - you just won't have to override cache.insert in the future and we'll provide this new kind of the flush in Emotion itself.

Note that you should only use Emotion with the so-called client components (we might add "use client"; directive to our files to make this obvious). They can render on the server just fine - for the initial SSRed/streamed HTML but server "refetches" won't render them on the server, they might be rendered on the client when the "refetch" response comes back. This is not Emotion's limitation - it's a limitation for all CSS-in-JS libs like Emotion (including Styled Components, styled-jsx and more)

Andarist avatar Nov 18 '22 09:11 Andarist

@Andarist Can you provide a CodeSandbox, please? I am getting the React.createContext is not a function error in the console even after wrapping my app with RootStyleRegistry

snelsi avatar Nov 18 '22 09:11 snelsi

https://stackblitz.com/edit/vercel-next-js-agrjfe?file=app%2FEmotionRootStyleRegistry.tsx,app%2Flayout.tsx,app%2Fpage.tsx

Andarist avatar Nov 18 '22 10:11 Andarist

Happy to see progress on here! I was wondering if anyone could see jsxImportSource working in compiler (ts swc or babel)?

From my understanding, I don't think it will be possible. Instead, we might have to use the /** @jsxImportSource @emotion/react */ comment (per file).

fcisio avatar Nov 18 '22 15:11 fcisio

With Babel it's definitely possible - you might need to set the runtime: 'automatic' first though: https://babeljs.io/docs/en/babel-preset-react#runtime

Andarist avatar Nov 18 '22 15:11 Andarist

Thanks @Andarist I'll test with Babel in a little bit. I did test with TS SWC just now (with the stackblitz above), and that one doesn't seem to work.

ScreenShot 2022-11-18 at 10 57 46 AM

fcisio avatar Nov 18 '22 16:11 fcisio

Note that you should only use Emotion with the so-called client components (we might add "use client"; directive to our files to make this obvious). They can render on the server just fine - for the initial SSRed/streamed HTML but server "refetches" won't render them on the server, they might be rendered on the client when the "refetch" response comes back. This is not Emotion's limitation - it's a limitation for all CSS-in-JS libs like Emotion (including Styled Components, styled-jsx and more)

@Andarist What does this mean? Surely we can add emotion styles to our react server components? What value does a react server component have when it doesn't have any styles? Maybe I don't fully understand :)

paales avatar Nov 29 '22 14:11 paales

Yeah, this is a little bit confusing - essentially server components have some limitations (for good reasons, in this model). They also have some unique capabilities (such as being able to use async functions).

I hope that this answer can help to clarify the situation a little bit. Let me know if you have any further questions.

Andarist avatar Nov 29 '22 16:11 Andarist

Hmm. Are we saying that Emotion will not be supporting styling server components? If so, we should be clear about this, as it is possibly reason enough to move off Emotion for some folks. My understanding is that server components do work with CSS modules, for example.

So for clarity's sake, we should probably not be discussing:

  • Data fetching components that do not involve CSS
  • MUI Base components that do not involve styling
  • MUI components that involve user interactivity (clearly need to be client components by definition)

The case that I think we're eager to understand is for basic static components that do things like layout and do require styling to achieve that. Do we currently — and will we long term — have a gap where CSS Modules could work for styling these components but Emotion would not?

adamrneary avatar Nov 29 '22 18:11 adamrneary

The case that I think we're eager to understand is for basic static components that do things like layout and do require styling to achieve that. Do we currently — and will we long term — have a gap where CSS Modules could work but Emotion would not?

Correct me if I'm wrong, but my understanding is that server and client components will both "work" in a SSR context, the only difference being that client components will also re-render and hydrate the client-side React tree. If you want to style server components (i.e. components which do not hydrate client-side) because that performance gain is important to you, then that may be a reason for you to ditch Emotion. If you are OK with hydration as is the current status quo, then you have nothing to be concerned about.

Is this incorrect?

stefee avatar Nov 29 '22 19:11 stefee

Yes, we are trying to understand if we can style server components (i.e. components which do not hydrate client-side). Before figuring out if this is a reason to ditch Emotion, we want to understand if this is in fact the near- to mid-term plan.

adamrneary avatar Nov 29 '22 19:11 adamrneary

If you want to style server components (i.e. components which do not hydrate client-side) because that performance gain is important to you, then that may be a reason for you to ditch Emotion.

This the main benefit of RSC to offload work to the server instead of having to do it on the client and have a performance gain? This effectively makes emotion useless in the future? (and will create a few thousand hours of work to migrate to something else..)

Maybe this conclusion is premature, but I really don't follow. It can not be that emotioncss can not be used with react server components. Styling needs to happen in a RSC and the resulting HTML/CSS should be send to the browser. That is the whole idea behind server components?

paales avatar Nov 30 '22 09:11 paales

If you want to style server components (i.e. components which do not hydrate client-side) because that performance gain is important to you, then that may be a reason for you to ditch Emotion.

This the main benefit of RSC to offload work to the server instead of having to do it on the client and have a performance gain? This effectively makes emotion useless in the future? (and will create a few thousand hours of work to migrate to something else..)

Maybe this conclusion is premature, but I really don't follow. It can not be that emotioncss can not be used with react server components. Styling needs to happen in a RSC and the resulting HTML/CSS should be send to the browser. That is the whole idea behind server components?

I think it's a misunderstanding. Next.js with SSR already does render styled components on the server. However styled components do not work with react server components. But styled components work in client components. Client components can be rendered server side with Next. So you already have a performance gain from that if that's what you're looking for. What React Server Components brings to the table is being able to natively call server functions.

You can verify this by writing a client component with emotion or styled components inside, and check your network when you have loaded your page in your browser. Check the reponse content of your first request which rendrs the html of your page, it already has your css.

React server Components are still new, I think it's a bit early to ditch styled components because they dont yet render in RSC.

KaddieZ avatar Dec 01 '22 15:12 KaddieZ

we are trying to understand if we can style server components

The question is answered already here.

Server Components were designed in a way that is at fundamental odds with CSS-in-JS. So don't expect Emotion (or other popular CSS-in-JS libs) to introduce support for them. They just don't allow us to integrate with them anyhow.

I am considering the question, why do we use a library like Emotion or Material or Chakra, etc. in the first place. A big benefit we get is reactivity, i.e. we can link the styles directly with React state. This is what enables us to have dynamic light/dark mode settings without page reload, for example. I think it fundamentally will not work to use these libraries in RSC because there is no reactivity or state in RSC - i.e. the goals of these libraries are juxtaposed to the goals of RSC.

However - to repeat what has been said already - since server Components can render client components, this does not mean Emotion cannot be used with server components, it only means that Emotion cannot be used directly by server components, if this makes sense.

stefee avatar Dec 01 '22 16:12 stefee