Skip to content
This repository has been archived by the owner on Feb 19, 2022. It is now read-only.

Document how to handle CSS, CSS-in-JS SSR #39

Open
jaredpalmer opened this issue Feb 15, 2017 · 28 comments
Open

Document how to handle CSS, CSS-in-JS SSR #39

jaredpalmer opened this issue Feb 15, 2017 · 28 comments

Comments

@jaredpalmer
Copy link
Contributor

jaredpalmer commented Feb 15, 2017

No description provided.

@jaredpalmer
Copy link
Contributor Author

jaredpalmer commented Feb 15, 2017

I got rapscallion working with glamor/server. Will submit a pr soon.

@aweary
Copy link
Contributor

aweary commented Feb 15, 2017

@jaredpalmer can you share how you did that?

@jaredpalmer
Copy link
Contributor Author

jaredpalmer commented Feb 16, 2017

// server.js 
import 'source-map-support/register';
import express from 'express';
import compression from 'compression';
import path from 'path';
import React from 'react';
import { withAsyncComponents } from 'react-async-component';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { renderStatic } from 'glamor/server';
import template from './template';
import App from '../components/App';
import configureStore from '../store';
import { render } from 'rapscallion';

/* eslint-disable */
const clientAssets = require(KYT.ASSETS_MANIFEST);
/* eslint-enable */
const server = express();

// Remove annoying Express header addition.
server.disable('x-powered-by');

// Compress (gzip) assets in production.
server.use(compression());

// Setup the public directory so that we can server static assets.
server.use(express.static(path.join(process.cwd(), KYT.PUBLIC_DIR)));

// Setup server side routing.
server.get('*', (request, response) => {
  // First create a context for <StaticRouter>, which will allow us to
  // query for the results of the render.
  const reactRouterContext = {};

  const store = configureStore({
    sourceRequest: {
      protocol: request.headers['x-forwarded-proto'] || request.protocol,
      host: request.headers.host,
    },
  });

  const ReactRoot = (
    <StaticRouter location={request.url} context={reactRouterContext}>
      <Provider store={store}>
        <App />
      </Provider>
    </StaticRouter>
  );

  withAsyncComponents(ReactRoot)
    .then(({ appWithAsyncComponents, state, STATE_IDENTIFIER }) => {
      const html = template({
        root: renderStatic(() => render(appWithAsyncComponents)),
        initialState: store.getState(),
        jsBundle: clientAssets.main.js,
        cssBundle: clientAssets.main.css,
      });

      if (reactRouterContext.url) {
        response.writeHead(302, { Location: reactRouterContext.url });
        response.end();
        return;
      }
      html.toStream().pipe(response);
    })
    .catch(e => {
      console.log(e);
      response.status(500).json({ error: e.message, stack: e.stack });
    });
});

server.listen(parseInt(process.env.PORT || KYT.SERVER_PORT, 10), err => {
  if (err) {
    throw err;
  }
  console.log('> started');
});
// template.js
/* eslint-disable prefer-template, max-len */
import { template } from 'rapscallion';

export default vo => template`

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
    <meta charSet='utf-8' />
    <title>Universal React</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="theme-color" content="#584095" />
    <meta name="mobile-web-app-capable" content="yes"/>
    <meta name="apple-mobile-web-app-capable" content="yes"/>
    <meta name="apple-mobile-web-app-title" content="NKBA"/>
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
    <link id="favicon" rel="shortcut icon" href="/kyt-favicon.png" sizes="16x16 32x32" type="image/png" />
    <link rel="apple-touch-icon" sizes="180x180" href="/icon/apple-touch-icon.png"/>
    <link rel="manifest" href="/manifest.json"/>
    <meta name="msapplication-tap-highlight" content="no"/>
    <meta name="msapplication-TileImage" content="/icon/ms-touch-icon-144x144-precomposed.png"/>
    <meta name="msapplication-TileColor" content="#F2F2F2"/>
    <meta name="theme-color" content="#673AB8"/>
    <style type="text/css">${() => vo.root.css}</style>
    <script type="text/javascript" async>
    !function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="4.0.0";
      analytics.load("XXXXXXXXXXXXX");
      }}();
    </script>
  </head>
  <body>
    <div id="root"><div>${vo.root.html}</div></div>
    <script src="${() => vo.jsBundle}" defer></script>
    <script type="text/javascript">window._initialState = ${() =>
  JSON.stringify(vo.initialState) ||
    {}}; window._glam =${() => JSON.stringify(vo.root.ids)};</script>

  </body>

</html>

`;
// client.js
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { withAsyncComponents } from 'react-async-component';
import { Provider } from 'react-redux';
import configureStore from '../store';
import ReactHotLoader from './RHL';
import App from '../components/App';

import { rehydrate } from 'glamor';
rehydrate(window._glam);

const container = document.querySelector('#root');
const store = configureStore(window._initialState || {});

function renderApp(TheApp) {
  const app = (
    <ReactHotLoader>
      <Provider store={store}>
        <BrowserRouter>
          <TheApp />
        </BrowserRouter>
      </Provider>
    </ReactHotLoader>
  );

  // We use the react-async-component in order to support super easy code splitting
  // within our application.  It's important to use this helper
  // @see https://github.com/ctrlplusb/react-async-component
  withAsyncComponents(app).then(
    ({ appWithAsyncComponents }) => render(appWithAsyncComponents, container),
  );
}

// // The following is needed so that we can support hot reloading our application.
if (process.env.NODE_ENV === 'development' && module.hot) {
  // Accept changes to this file for hot reloading.
  module.hot.accept('./index.js');
  // Any changes to our App will cause a hotload re-render.
  module.hot.accept(
    '../components/App',
    () => renderApp(require('../components/App').default),
  );
}
// Execute the first render of our app.
renderApp(App);

@jaredpalmer
Copy link
Contributor Author

jaredpalmer commented Feb 16, 2017

@aweary still working through the checksum. currently has a double flicker, but the styles are there and 85/100 lighthouse on now.sh ain't too shabby.

@jaredpalmer
Copy link
Contributor Author

@aweary if i add

document.querySelector("#root").setAttribute("data-react-checksum", "${vo.root.html.checksum()}")

to template.js(right after the initialState declaration), I get the following error:

Error: Renderer#checksum can only be invoked for a renderer converted to node stream.
    at Renderer.checksum (/Users/jared/workspace/XXXX/node_modules/rapscallion/src/renderer.js:31:13)
    at exports.default (/Users/jared/workspace/XXXX/build/server/webpack:/src/server/template.js:41:89)
    at /Users/jared/workspace/XXXX/build/server/webpack:/src/server/index.js:53:20

@divmain
Copy link
Contributor

divmain commented Feb 16, 2017

@jaredpalmer I think there may still be issues with the order of evaluation of the template segments. I'll reproduce this example this evening and make any edits that seem necessary. In particular, glamor's renderStatic looks a bit funky (it relies on a global singleton and assumes synchronous render) and will have to be worked around. But it's definitely doable.

@divmain
Copy link
Contributor

divmain commented Feb 16, 2017

I'm also thinking through how I want to document all of this. It might become cumbersome to document how-tos for individual React libraries. But these are definitely questions that people will be asking, so a FAQ that covers some of these common patterns might be really useful.

@jaredpalmer
Copy link
Contributor Author

As cumbersome as it might be to do recipes for individual libs, it would drive adoption if things were copy and paste-ready though.

@divmain
Copy link
Contributor

divmain commented Feb 17, 2017

That's very true. The problem with full examples in the docs is that they're hard to keep up-to-date. Blog posts don't usually have that problem - people expect that they may be incorrect after awhile.

On the other hand, it might be worth including well-commented examples in the repo. They can be rendered in CI, and that way we'll have some guarantees regarding correctness. Do you think that would suffice @jaredpalmer?

@jaredpalmer
Copy link
Contributor Author

I think there are really 4 big recipes:

  • RR v3 match() {....}
  • RR v4 StaticRouter etc.
  • Redux
  • One CSS-in-JS

You could probably combine the Redux and CSS-in-JS into one.

@wmertens
Copy link
Contributor

wmertens commented Mar 2, 2017

Also, injecting stuff in <head/> with e.g. react-helmet, for example meta tags and document title.

@bkniffler
Copy link

I'd really love to see such demos, still wondering how to make this working with apollo and fela!

@threepointone
Copy link

I wrote a transforming stream that takes streamed html and inlines (glamor) css at the precise points its used.

Uncovered a bug in rapscallion in that it doesn't read glamor rules attached via data attribs. classnames work fine though. Will file separate issue for it.

If you're adventurous, using glamor with classnames, copy paste this file somewhere, and use as -

import inline from './path/to/file'
// ...
render(<App/>).pipe(inline()).pipe(res)  

as a bonus, this method has the benefit of not needing to inline css and ids at any point, and should "just work".

@wmertens
Copy link
Contributor

wmertens commented Mar 13, 2017 via email

@threepointone
Copy link

Pretty much, yes. I have naive detection for classnames, and prepend a style tag before its first occurrence.

Unsure which tags it would break with, I'll try out tables and such.

It does break checksums. React 16 doesn't show the warnings tho 🤷‍♀️

@threepointone
Copy link

It appears the checksums break only when used with rapscallion. I might be integrating wrong. I'll try some variations and get back here.

@ismay
Copy link

ismay commented May 10, 2017

For people who are wondering how to use rapscallion and styled-components (also a css-in-js lib) for SSR, this is how I'm doing it: https://github.com/ismay/ismaywolff.nl/blob/develop/src/server/handleRender.jsx#L18

Works like a charm.

@wmertens
Copy link
Contributor

wmertens commented May 10, 2017 via email

@ismay
Copy link

ismay commented May 10, 2017

Just making sure: This first renders the entire app and then streams it,
right?

@wmertens Hmm, I hope not. I've based it on the example from the readme: https://github.com/FormidableLabs/rapscallion#example. So I hope I'm not doing anything stupid and losing all of rapscallions benefits..

The way I intended this to work is for it to start streaming immediately, and while it's doing that render <App />. Only thing it should be waiting for is for the promises from fetch to resolve and for each previous expression to be evaluated. Which is also how I understood the example from the readme to work, but I might be misunderstanding that.

But yeah, since the styles are only available after rendering <App /> you'll have to insert them in the <head /> clientside. Just like a couple of other things need to be handled clientside (like setting meta tags, checksum, etc.).

@siddharthkp
Copy link

@ismay I don't think you'll be able to get the styles as well while streaming. Related: styled-components/styled-components#600 (comment)

@ismay
Copy link

ismay commented May 10, 2017

@siddharthkp, That's ok, as long as I can get them afterwards and then insert them in the head clientside.

@siddharthkp
Copy link

⚠️ Probably not the right issue for streaming + css-in-js discussion, let me know if I should create another issue.

@ismay Are you saying you will get the styles at the end of the streaming? If yes, the true benefit of stream is not realised.

@ismay
Copy link

ismay commented May 10, 2017

Are you saying you will get the styles at the end of the streaming? If yes, the true benefit of stream is not realised.

@siddharthkp Currently yes. Anything else isn't possible with styled-components v2's api as far as I know. It'd be nice if the styles could be attached right away yeah, but there are still benefits to streaming nonetheless. I can start sending a response right away which enables the client to start downloading assets (etc.) earlier than if I'd render everything on the server and then start sending. The styles might be missing, but there's still other content besides that.

@kitten
Copy link

kitten commented May 10, 2017

@ismay we could potentially place the style tags at the end of the body to bring streaming back properly, right?

@siddharthkp
Copy link

@philpl, I'm not sure about that. I imagine the real power is rendering above the fold content first and then follow it up by rest. that would include styles.
styles coming at the end is not optimal

@wmertens
Copy link
Contributor

wmertens commented May 10, 2017 via email

@ismay
Copy link

ismay commented May 11, 2017

we could potentially place the style tags at the end of the body to bring streaming back properly, right?

@philpl I'm not sure if that's valid. Browsers might parse it but I believe only the scoped attribute allows style tags in the body and scoped has been deprecated. Placing them in the head seems the most safe to me. Please correct me if I'm wrong.

And as @siddharthkp said, when streaming the most desirable implementation would be to stream html that includes styles as well so that there's no unstyled content. @wmertens' suggestion would be a way to achieve that, if it's possible.

(above-the-fold rendering is awesome for users, bad for search engines. Only do it when you detect a regular visitor)

@wmertens That got me thinking, hope I'm not derailing the issue (and let me know if I am), but I was thinking about how to handle this. Maybe set a cookie, and:

  1. If user is a returning visitor, just send an as simple as possible reply without fetching data and let the client render everything (since they have all resources already, and fetched data can be cached clientside), so it'll be fastest if the user renders it all locally.
  2. If user is new or has a stale cache, fetch data and stream to client with toStream(), to make sure content is visible as soon as possible.
  3. Maybe try to detect if user is a bot (https://github.com/biggora/express-useragent), and render completely with toPromise() before sending a reply, since there's not anyone waiting and accuracy is more important.

That would mean three different approaches for rendering. Maybe a good subject for a new issue where we can discuss adapting rendering to the request (and adding that to the readme).

@jamesjjk
Copy link

jamesjjk commented Jun 13, 2017

@jaredpalmer @wmertens Would be great to see how you can stream react markup and inject into the head.

I tried a similar approach to @threepointone - inlinePipe and was able to extract the head tag data generated by helmet however I would require to write some additional functionality to modify the top down order of the template renderer. Not sure if this is the correct approach, would be nice to use some type of callback or promise on the functions to sync the stream. Similar to your renderer. Specifically interested in putting data in the head (react-helmet) and retaining the benefit of a stream.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants