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

CSS-in-JS Rendering Hooks #3236

Open
lunelson opened this issue Aug 7, 2020 · 26 comments
Open

CSS-in-JS Rendering Hooks #3236

lunelson opened this issue Aug 7, 2020 · 26 comments
Labels
difficulty: advanced Issues that are complex, e.g. large scoping for long-term maintainability. proposal This issue is a proposal, usually non-trivial change
Milestone

Comments

@lunelson
Copy link

lunelson commented Aug 7, 2020

πŸ’₯ Proposal: Hooks for CSS-in-JS Client and Server Rendering

  • Guides / Styling and Layouts β†’ Styling components with CSS-in-JS frameworks

So, the [v2] β˜‚οΈ Umbrella issue for v2 docs has the above open TODO (although the issue has been closed); and this is also currently mentioned in the docs as "welcoming PRs".

I have some experience with the setup patterns for emotion (incl. v11) and styled-components here, and would like to help with this. As far as I can tell from the source, this would require additions to the Lifecycle APIs so that the serverEntry.js and clientEntry.ts files' rendering functions could be modified. I would need some guidance on creating these hooks.

Anyone want to help me get started?

@lunelson lunelson added status: needs triage This issue has not been triaged by maintainers proposal This issue is a proposal, usually non-trivial change labels Aug 7, 2020
@lunelson lunelson changed the title CSS-in-JS Rendering Hooks [v2] CSS-in-JS Rendering Hooks Aug 7, 2020
@slorber
Copy link
Collaborator

slorber commented Aug 8, 2020

Hi,

This is definitively something we should have, but is not so easy.
We also need to extract critical CSS to inline it in the pages.

It's already been mentioned somewhere that we may need to do something similar to how Gatsby works (docusaurus-browser/docusaurus-ssr files...).

I don't have much more infos to provide, this needs exploration :)

Maybe a good start would be to study all the css-in-js gatsby plugins, and maybe create some kind of doc/spreadsheet, to see what are the useful lifecycles / api surface to provide to make it possible to support a wide range of CSS-in-JS libraries.

@slorber slorber added difficulty: advanced Issues that are complex, e.g. large scoping for long-term maintainability. and removed status: needs triage This issue has not been triaged by maintainers labels Aug 8, 2020
@lunelson
Copy link
Author

lunelson commented Aug 8, 2020

Yes, off the top of my head the necessary operations for CSS-in-JS libs tend to be:

  • wrap the <App /> component with custom Providers which enable collection and critical extraction
  • set <head/> elements with those extracted styles, per page
  • extend the babel config, usually to provide a custom JSX function
  • ...and in the case of exctraction libs like astroturf and linaria: extend the webpack config with custom loaders

The latter 3 are all supported in Docusaurus in one way or another AFAICT; what's really missing is wrapping the <App /> element.

In Gatsby as I'm sure you know, this is done through the wrapRootElement and/or wrapBodyElement functions, which are exported from gatsby-ssr.js and gatsby-browser.js.

It seems to me, that these correspond to the serverEntry.js and clientEntry.ts files I mentioned above... so I'm thinking some plugin APIs like wrapServerRootElement and wrapClientRootElement, as long as they could also provide a way to set Head elements, would do the trick

@lunelson
Copy link
Author

lunelson commented Aug 8, 2020

Examples from Gatsbyβ€”note: many only require a babel plugin for the client side, and only modify rendering for the SSR part

@lunelson
Copy link
Author

@slorber on closer examination, after trying to hack this today: I'm guessing when you said "it's not so easy" you might have been thinking about react-loadable...? This scheme doesn't seem so far to be compatible with the HOCs that are needed for collecting CSS from the component tree

@slorber
Copy link
Collaborator

slorber commented Aug 11, 2020

Thanks for the details @lunelson

I don't have time to investigate these things right now but we'll likely come back to it once i18n is there

@slorber
Copy link
Collaborator

slorber commented Aug 17, 2020

Note, we might introduce a way to wrap root element in this PR: #3153

@lunelson
Copy link
Author

@slorber yes functions for wrapping root and/or page element would also solve #2891. I see that you've indicated it as low-priority, but IMO it's rather important, e.g. for components or context-providers that may be maintaining global state and doing async operations, such as an analytics consent-manager which is causing me trouble at the moment

@slorber
Copy link
Collaborator

slorber commented Oct 22, 2020

@lunelson in the meantime alpha66 declare a new component "LayoutProviders" that you can swizzle and wrap with custom providers.

For minimum maintenance burden, you can also check this doc to "enhance" an existing theme comp without duplicating its code: https://v2.docusaurus.io/docs/using-themes/#wrapping-theme-components

But I understand the issue about stateful providers resetting their states. We'll try to solve this soon after I finish i18n.

@lunelson
Copy link
Author

Thanks for the tips @slorber, good to know about those new components, but indeed yes the remounting-on-route-change problem remains open

@sawasawasawa
Copy link

@slorber @lunelson Could you suggest any workaround to avoid a flash of unstyled content using docusaurus and styled-components (or any other CSS-in-JS lib as far as I understand)?
I would be happy to help implementing a real solution if you could suggest some direction.
An example of the problem I am facing with the code here (you might need to simulate a slower network to experience that)

@lunelson
Copy link
Author

lunelson commented Jan 19, 2021

@sawasawasawa yes I believe this is the essential problem with CSS-in-JS in docusaurus, that is the lack of server-side-rendering support. Normally in the SSR phase, CSS-in-JS rules should be collected and written in to the <head> of the document, via functions that wrap the <body>. AFAIK we don't have an API yet in docusaurus to enable this. The result is that styles are only created on the client side after hydration, hence the FOUC

@bennobuilder
Copy link
Contributor

So the fouc has something to do with docusaurus and not with styled components?
https://stackoverflow.com/questions/66212466/css-styles-get-applied-with-1s-delay-styled-components?noredirect=1#comment117063613_66212466

@slorber
Copy link
Collaborator

slorber commented Feb 16, 2021

It has something to do with both.
If you use server-side rendering like Docusaurus, Gatsby or Next, a CSS-in-JS like Styled-Components need proper integration with the framework to ensure critical CSS is inlined in the HTML files and avoid FOUC.
It is documented on SC here: https://styled-components.com/docs/advanced#server-side-rendering

@bennobuilder
Copy link
Contributor

ok.. so the issue can only be fixed in docusaurus itself?
I will take a look into this..
Wanted to find out a bit more about server side rendering anyway ^^

@bennobuilder
Copy link
Contributor

ok.. I investigated some time in fixing this issue..
but I couldn't figure out how to implement such thing in docusaurus..

so is there a plan in creating a styled-component integration for docusaurus in the near future
or should I rather switch to css modules :/

@lunelson
Copy link
Author

@bennodev19 the solution requires an API to be made available from docusaurus, similar to Gatsby's wrapRootElement and wrapPageElement APIs, or Next.js' _document.js and _app.js APIs, that enable intervention at the root of the application to provide as well as collect data. These APIs don't exist (yet)

@bennobuilder
Copy link
Contributor

bennobuilder commented Feb 17, 2021

yeah sure.. I tried to create such interface/api.. but I wasn't able to bring it to life..
so I wanted to know if other people (with more knowledge in this area)
have planned to invest some time in creating such api ^^
because if not I see no reason to stick to styled components for this project (agile-docs), since I want to launch/update the landing page with proper styling in the near future.
And that isn't possible with styled-components yet because also theme props are undefined.. (which of course makes sense)

@slorber
Copy link
Collaborator

slorber commented Feb 17, 2021

@bennodev19 that's something we want to work on but is not so simple.
APIs are forever so we'd rather find a good abstraction for CSS-in-JS integration that will work for all the libs, not just SC.
We could copy Gatsby's abstraction but it's also a good time to study what has worked well and not so well for Gatsby by studying all its CSS-in-JS plugins a bit.

You'd rather convert to css modules if you cannot wait because I don't think we should rush on implementation without a careful design.

@exah
Copy link

exah commented Mar 2, 2021

Hey guys,

I found a super hacky way to collect styles for styled-components, and I do not recommend it to anyone, but it works :). Most likely, it breaks hydration and forcing react to re-render the whole tree. Sadly, I don't know how to force the non-production build to check, but honestly, this is much better than FOUC.

I'm voting for public API, which will allow returning your element before passing it to renderToString from react-dom/server.

Hacky way
  1. Create component for collecting styles from children:
import React from 'react'
import { renderToString } from 'react-dom/server'
import { ServerStyleSheet } from 'styled-components'
import { StaticRouter, useLocation } from 'react-router-dom'
import { HelmetProvider } from 'react-helmet-async'
import { Context as DocusaurusContext } from '@docusaurus/core/lib/client/docusaurusContext'
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'

function ServerStyle({ from: children }) {
  let style = null

  const location = useLocation()
  const context = useDocusaurusContext()
  const sheet = new ServerStyleSheet()

  try {
    renderToString(
      sheet.collectStyles(
        <HelmetProvider>
          <StaticRouter location={location}>
            <DocusaurusContext.Provider value={context}>
              {children}
            </DocusaurusContext.Provider>
          </StaticRouter>
        </HelmetProvider>,
      ),
    )
    style = sheet.getStyleElement()
  } catch (error) {
    console.error(error)
  } finally {
    sheet.seal()
  }

  return style
}

function ClientStyle() {
  return null
}

export default typeof window === 'undefined' ? ServerStyle : ClientStyle
  1. And then render it in your Root:
// /src/theme/Root.js
import React from 'react'

import ServerStyle from './ServerStyle'

function Root({ children }) {
  return (
    <>
      <ServerStyle from={children} />
      {children}
    </>
  )
}

export default Root

UPD: Fixed DocusaurusContext imports
UPD (2022-05-06): No longer works for v2.0.0-beta.16+
UPD (2022-06-10): Fixed for v2.0.0-beta.21+ thanks to @EmaSuriano!

@adamborowski
Copy link

I think that we can add a plugin point here:
https://github.com/facebook/docusaurus/blob/main/packages/docusaurus/src/client/serverEntry.tsx#L88
It basically renders whole app so we can add a plugin lifecycle method that can customize somehow this method,
and in case of styled components the plugin should do sth like this:

plugin.serverSideRender = (renderToString, element /*wholeTreeWithLoadableHelmetProviderStaticRouterEtc*/)=>{
const sheet = new ServerStyleSheet();
const html = renderToString(sheet.collectStyles(element))
  const headTags = sheet.getStyleTags() 

return {headTags, html}

}

@slorber
Copy link
Collaborator

slorber commented Feb 9, 2022

Yes, that seems like a reasonable thing to do.

I'd be interested if someone could help design an API that works for most CSS-in-JS libs, and also allow composition (ie one plugin provides its CSS-in-JS integration, and you can use 2 CSS-in-JS libs by combining 2 plugins)

This requires studying the ecosystem a bit.

I'd like to have a table with expected CSS-in-JS libs compatibility before merging a PR.

This is the Gatsby API: https://www.gatsbyjs.com/docs/reference/config-files/gatsby-ssr/#replaceRenderer

@Josh-Cena Josh-Cena modified the milestones: 3.0.0, 2.x Apr 19, 2022
@cieldon32
Copy link

cieldon32 commented May 27, 2022

en...
I can use styled-components
I use "babel-plugin-styled-components",
and then, use in the part of render, it is work .

@ntucker
Copy link
Contributor

ntucker commented Aug 1, 2022

I would recommend considering https://github.com/callstack/linaria as it is much better for performance than others.

@priley86
Copy link

priley86 commented Oct 24, 2022

hello Docusaurus friends πŸ‘‹

I wanted to report having the same issue here w/ rendering any styled-components in my extended/swizzled Docusaurus templates. This caused a good deal of flicker and in some cases the CSS did not load initially at all. As of now, the workaround @exah describes above is working well for me when I render these components separately during the Layout rendering when the window is not yet available. Some Docusuarus components are SSR safe, but others which rely on Scroll Context or Announcement Context etc do not seem to be at the moment. Just sharing for anyone else facing this challenge.

(swizzled theme-common Layout):

import { Header } from './src/Header'; // custom header
import { ThemeProvider } from '@mui/material/styles';
import ErrorBoundary from '@docusaurus/ErrorBoundary';
import Head from '@docusaurus/Head';
import { PageMetadata, ThemeClassNames } from '@docusaurus/theme-common';
import AnnouncementBar from '@theme/AnnouncementBar';
import ErrorPageContent from '@theme/ErrorPageContent';
import Footer from '@theme/Footer';
import LayoutProvider from '@theme/Layout/Provider';
import Navbar from '@theme/Navbar';
import SkipToContent from '@theme/SkipToContent';
import clsx from 'clsx';
import React from 'react';
import { ThemeProvider as SCThemeProvider } from 'styled-components';

import { getTheme } from './my-mui-theme/theme';
import ServerStyle from './ServerStyle'; // workaround described above from @exah
import styles from './styles.module.css';
import { useKeyboardNavigation } from './useKeyboardNavigation';

const themeProps = { theme: getTheme() };

export default function Layout(props: any) {
  const {
    children,
    noFooter,
    wrapperClassName,
    // Not really layout-related, but kept for convenience/retro-compatibility
    title,
    description,
  } = props;
  useKeyboardNavigation();

  const header = (
    // ssr/styled-components header
    <Header/>
  );

  if (typeof window === 'undefined') {
    // extracts styles from server renderable content (e.g. contents w/ styled-components)
    // for initial css load. Some Docusaurus contents are not currently SSR safe.
    const ssrContent = (
      <ThemeProvider {...themeProps}>
        <SCThemeProvider theme={themeProps.theme}>
          <PageMetadata title={title} description={description} />
          {header}
          <div className={clsx(ThemeClassNames.wrapper.main, styles.mainWrapper, wrapperClassName)} />
        </SCThemeProvider>
      </ThemeProvider>
    );

    return <ServerStyle from={ssrContent} />;
  }
  return (
    <LayoutProvider>
      <ThemeProvider {...themeProps}>
        <SCThemeProvider theme={themeProps.theme}>
          <PageMetadata title={title} description={description} />
          <SkipToContent />

          <AnnouncementBar />

          {header}

          <div className={clsx(ThemeClassNames.wrapper.main, styles.mainWrapper, wrapperClassName)}>
            <ErrorBoundary fallback={(params) => <ErrorPageContent {...params} />}>{children}</ErrorBoundary>
          </div>

          <Footer />
        </SCThemeProvider>
      </ThemeProvider>
    </LayoutProvider>
  );
}

@RudraSen2
Copy link
Contributor

I'm commenting on this without thinking - we can use https://stitches.dev

@slorber
Copy link
Collaborator

slorber commented May 4, 2023

The usage of React Server Components is growing, and we plan to adopt them in Docusaurus.

And we see that the past generation of runtime-based CSS-in-JS libs does not play with React Server Components (more explanations here: #8959).

At this point I'm not even sure we need to have these hooks anymore, it goes against the industry trend to adopt no-runtime solutions.

Many existing libs are in the process of figuring out how to benefit most from Server Components, so I think we should wait and decide later what is best for Docusaurus.

For the moment, I'm leaning towards not implementing these hooks because I believe it wouldn't encourage the usage of performant code by default in the long term, and the only good reason to implement those is to be compatible with legacy UI libs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
difficulty: advanced Issues that are complex, e.g. large scoping for long-term maintainability. proposal This issue is a proposal, usually non-trivial change
Projects
None yet
Development

No branches or pull requests