emotion-js/emotion

Emotion in React Server Components?

karlhorky opened this issue · 12 comments

The problem

I would like to have a Next.js 13 Layout component (eg. RootLayout) that is a Server Component, and have my styling for that layout also stay in Server Components (potentially even in the same app/layout.tsx file). I only want to switch to a Client Component if there is some client-only features I'm using such as useState.

This also aligns with the current guidance (as of Jan 2023) from the Next.js team to just leave everything a server component until you absolutely need a client component.

Proposed solution

It would be great if Emotion would be able to be used in React Server Components (without conversion to a Client Component)

Alternative solutions

  1. Only a subset of Emotion could be supported in React Server Components - eg. the part that is statically extractable / zero-runtime
  2. Use Client Components via 'use client' every time that a page should be styled
  3. Switch to a different styling solution which can be used in React Server Components (eg. CSS Modules)

Additional context

Moved from this other issue about Next.js 13 app/ directory support:

🤔 Wonder if this new "Float" thing has anything that is useful here for enabling CSS-in-JS + React Server Components

Screenshot 2023-01-30 at 16 12 55

cc @sebmarkbage

Some other CSS-in-JS ecosystem movement trying to support React Server Components:

Seems like Chakra UI is going to switch away from Emotion and to a zero runtime CSS-in-JS solution called Panda:

Zero runtime CSS-in-JS [Panda]

This is the most common and most challenging request we get from users.

Runtime CSS-in-JS and style props are powerful features that allow developers to build dynamic UI components that are composable, predictable, and easy to use. However, it comes at the cost of performance and runtime.

With the release of React Server Components, providing the ability to write Chakra UI components on the server has become crucial. This is a huge win for performance, development, and user experience.

We're building a new, framework-agnostic styling solution that keeps most of the great features of Chakra's styling system but extracts styles at build time. It'll also feature a PostCSS plugin that extracts styles at postcss run time during development.

Panda will leverage new modern platform features like CSS variables, cascade layers, and W3C token spec.

https://www.adebayosegun.com/blog/the-future-of-chakra-ui

paales commented

Too ease the blow for everybody who is struggling with this (including me), a little consideration:

If you were happy with emotion before, there is no difference right now. Client components render the same way, they render with SSR and are passed to the browser and render there as well.

The difference is that the leaves are bit higher up and you need to use wrapper for direct HTML elements.

Lot's of elements that do not actually render HTML elements can still use RSC, this means all your components that composite other components, like pages or layouts still benefit from RSC.

import { Box } from '@mui/material' // If you've patched @mui/material to have a 'use client' on top of the actual JS file.

export async function ThisIsAServerComponent() {
  const bla = await fetch()

  return <Box sx={{color: 'red'}}>Hello from RSC {bla}</Box>
}

There of course are long therm choices to be made to move to a build-time extraction of css-in-js or another solution, but this argument was always there.

@Andarist we are migrating to Next.js app router and using server components by default, but are currently blocked by not knowing if MUI/emotion will be compatible with server components. We'd really appreciate confirmation on whether to expect MUI/emotion to support server components in the future and roughly when that could be possible, so we can plan and start migrating.
I've outlined the compatibility issues with MUI and server components, (naive) possible solutions and the blocked decision we're facing - whether to use MUI going forward. Lots of which has been discussed on the main MUI thread already but summarising again in case it's helpful for others reaching this thread!

Compatibility issues

CSS-in-JS

Server components are not currently compatible with CSS-in-JS, a core feature of MUI and most component libraries, due to their closely coupled dependency, emotion.
Dependency tree: MUI → emotion → CSS-in-JS

Next.js Docs highlight that CSS-in-JS is not currently supported and that Next.js/React are working on supporting this, although it’s not clear if CSS-in-JS will be supported for server components by Next.js/React alone, or more likely that library authors will also need to make changes. The next.js docs also state that MUI and emotion are also working on support, but there’s no confirmation of this or definitive answers about whether server components will be supported by MUI/emotion in the future.

MUI’s options
Existing issue - #34826

  • Don’t support server components
  • Remove emotion in a significant refactor, similar to Chakra who are building a new library
  • Provide alternative server compatible components, e.g. switch/compile to CSS modules or use subset of emotion features

Emotions options

  • Don’t support server components
  • Remove CSS-in-JS in a significant refactor
  • Provide alternative solutions for server components, e.g. switch/compile to CSS modules or use subset of emotion features

Dynamic components

MUI components use interactivity including event listeners, hooks, state and lifecycle effects. Inherently making them client components and not compatible with server components. For some MUI components this interactivity is always a requirement (e.g. all inputs, <Accordion>, <Tooltip>). However we also want to use non-interactive UI components in our server components - e.g. static text using <Typography> or a basic styled <Card> component with no actions. Even <Button> components can be used in server components when using a static href link vs an onClick() listener. Currently it isn’t possible to use MUI components in server components regardless of if we are using interactive/stateful features of a MUI component, because the listeners/hooks are still defined in the MUI component.

MUIs options
Existing issue - #35993

  • Provide alternative MUI server-compatible components, removing the interactive or stateful functionality and relying purely on serialisable props.
    • e.g. Client component <Box> and server component <ServerBox>
    • e.g. Client component import { Box } from '@mui/material'; and server component import { Box } from '@mui/material/server';
  • Add functionality to switch component or strip listeners/hooks from a component if e.g. a server prop is present on the component <Box server={true}> or automatically detected server component

Current server components + MUI project migration options

For those intending to use server components on a new project, or refactoring an existing project using MUI, the outlook/decision currently seems to be:

  1. Assume (a) MUI will support server components and/or (b) Next.js/React will find a solution to handling JS-in-CSS, and continue building with MUI. Leave all components as client for now, then add back server components when MUI supports them. Tradeoff: likely refactoring required.
  2. Assume MUI will not support server components for the foreseeable future. Build all UI in client components only and continue building with MUI. Use server components only as wrappers to provide data to UI client components, or don’t use server components at all. Tradeoff: losing performance benefits of server components, gaining MUI.
  3. Remove MUI as the general component library and replace with a CSS solution like Tailwind which works well with server components. MUI could still be used for complex components like date pickers and advanced grids, which are client components anyway by their interactivity; however setting up theming for two different styling solutions is not ideal. Tradeoff: losing MUI, gaining server components performance benefits.

Disclaimer: I'm not an RSC expert so take this with a grain of salt.

The next.js docs also state that mui/material-ui#34905 and #2928 are also working on support

Next.js rushed the release of their docs without consulting library authors. The mentioned Styled-Components "support" looks almost exactly the same as the Emotion support can look like (see the comment here). There is no special API in SC that integrates with RSC in any special way.

I expect that both SC and Emotion might be prone to subtle hydration bugs when using this approach though (for the majority of use cases they should be totally negligible. I don't want to raise panic over this as there is definitely nothing to panic about here, just mentioning for completeness and correctness. I don't want to claim that the mentioned support is ideal).

To bring better support to Emotion we are waiting for React's new APIs that should allow us to inject styles to the streamed response without having to "listen" on what's being streamed (this is essentially how useServerInsertedHTML works today).

However we also want to use non-interactive UI components in our server components - e.g. static text using or a basic styled component with no actions. Even components can be used in server components when using a static href link vs onClick() listener.

All of those are just client components - which doesn't mean that they can't be rendered on the server. Refetches might not benefit from SSR style-injection but that shouldn't be a big deal for the majority of use cases anyway. You can totally use those in RSC, as shown by @paales here

Currently it isn’t possible to use MUI components in server components regardless of if we are using interactive/stateful features of a MUI component, because the listeners/hooks are still defined in the MUI component.

This might require some changes in MUI. I'm not part of their team so you should raise that in their issue tracker. Likely the issue is just about adding 'use client'; to their entrypoints (something that Emotion should likely do as well?)

Thanks for the speedy reply @Andarist and apologies I thought you were also part of the MUI team!

To bring better support to Emotion we are waiting for React's new APIs

Can we read from this that server components will be supported by emotion/MUI in good time, or that's the intention? And its safe for us to continue building with MUI, switching those client components to server components when they're later supported?

All of those are just client components

Are you suggesting that all UI should be in client components, and we shouldn't be using UI components in server components? Apologies if I've misunderstood your response, but the issue is that we also want to render static UI in server components

If we were to split the page into smaller components, you'll notice that the majority of components are non-interactive and can be rendered on the server as Server Components. For smaller pieces of interactive UI, we can sprinkle in Client Components. This aligns with Next.js server-first approach. docs

Screenshot 2023-06-12 at 12 33 58

This whole style extraction thing sounds like a good chunk of work slowing down fast refresh and building times or am I just being overly anxious?

Housi commented
  1. Only a subset of Emotion could be supported in React Server Components - eg. the part that is statically extractable / zero-runtime

That would be similar to what react /next is doing, eg. not allowing for hooks in server components. If no hooks and no state are present, dynamic styling literally renders useless anyway... Im my opinion this is the way to go, especially considering that this would solve most of the 'performance problems' css-in-js is accused of 🍿

Having to use 2 styling libs in a project does not sound inviting imho, but it's the only way possible now 😞

As I understand it, the lack of the React Context API is the only reason why Emotion doesn't work with RSC.

What would prevent us from using the React 18 cache() API to replace the React context when running with RSC? It's scoped to the server-side render cycle. So to the request with Next.js. One example of a library wrapping it: https://github.com/manvalls/server-only-context.

So it seems to me that we can make Emotion work with RSC, today, I wish I had time to work on this 🙃.

@oliviertassinari just for the record I am not sure about the example in the readme of this lib, more info here : manvalls/server-only-context#4
But on the top of my head, for a lib like Emotion, what you want is a stateful object scoped to the current request React tree, so I think it would work.

I did a POC to test RSC + Client Side Component context support using React.cache on RSC and React.useContext on client components, it seems to work fine: joshwcomeau/dream-css-tool#7. The main trick is:

/* eslint-disable react-hooks/rules-of-hooks */
import * as React from 'react';

export const useServerCache = React.cache(() => []);

let clientContext;

function getClientCache() {
  if (!clientContext) {
    clientContext = React.createContext([]);
  }

  return clientContext;
}

export function useCache() {
  try {
    // React Server Component
    return useServerCache();
  } catch (e) {
    // React Client Component
    const clientContext = getClientCache();
    return React.useContext(clientContext);
  }
}

We need to duplicate the context value. cc @Andarist

How I understand things working:

SCR-20240221-oups