facebook / docusaurus

Easy to maintain open source documentation websites.
https://docusaurus.io
MIT License
56.14k stars 8.42k forks source link

CSS-in-JS Rendering Hooks #3236

Open lunelson opened 4 years ago

lunelson commented 4 years ago

💥 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?

slorber commented 4 years ago

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.

lunelson commented 4 years ago

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

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 commented 4 years ago

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

lunelson commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

Note, we might introduce a way to wrap root element in this PR: https://github.com/facebook/docusaurus/pull/3153

lunelson commented 3 years ago

@slorber yes functions for wrapping root and/or page element would also solve https://github.com/facebook/docusaurus/issues/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 commented 3 years ago

@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 commented 3 years ago

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

sawasawasawa commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

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`: ```js 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( {children} , ), ) style = sheet.getStyleElement() } catch (error) { console.error(error) } finally { sheet.seal() } return style } function ClientStyle() { return null } export default typeof window === 'undefined' ? ServerStyle : ClientStyle ``` 2. And then render it in your `Root`: ```js // /src/theme/Root.js import React from 'react' import ServerStyle from './ServerStyle' function Root({ children }) { return ( <> {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 commented 2 years ago

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 commented 2 years ago

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

cieldon32 commented 2 years ago

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

ntucker commented 2 years ago

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

priley86 commented 1 year ago

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 commented 1 year ago

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

slorber commented 1 year ago

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: https://github.com/facebook/docusaurus/discussions/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.