Giter Club home page Giter Club logo

react-server-renderer's Introduction

react-server-renderer

GitHub Actions npm GitHub Release

Conventional Commits Renovate enabled JavaScript Style Guide Code Style: Prettier changesets

Yet another simple React SSR solution inspired by vue-server-render with:

  1. Server bundle with hot reload on development and source map support
  2. prefetch/preload client injection with ClientManifest, generated by webpack-plugin inside
  3. server css support with react-style-loader
  4. Async component support with react-async-component and react-async-bootstrapper
  5. custom dynamic head management for better SEO

Real World Demo

react-hackernews

Usage

This module is heavily inspired by vue-server-render, it is recommended to read about bundle-renderer.

It uses react-router on server, so you should read about Server Rendering.

And also, data injection should be implement with asyncBootstrap.

Build Configuration

Server Config

import webpack from 'webpack'
import merge from 'webpack-merge'
import nodeExternals from 'webpack-node-externals'
import { ReactSSRServerPlugin } from 'react-server-renderer/server-plugin'

import { resolve } from './config'

import base from './base'

export default merge.smart(base, {
  // Point entry to your app's server entry file
  entry: resolve('src/entry-server.js'),

  // This allows webpack to handle dynamic imports in a Node-appropriate
  // fashion, and also tells `react-style-loader` to emit server-oriented code when
  // compiling React components.
  target: 'node',

  output: {
    path: resolve('dist'),
    filename: `[name].[chunkhash].js`,
    // This tells the server bundle to use Node-style exports
    libraryTarget: 'commonjs2',
  },

  // https://webpack.js.org/configuration/externals/#function
  // https://github.com/liady/webpack-node-externals
  // Externalize app dependencies. This makes the server build much faster
  // and generates a smaller bundle file.
  externals: nodeExternals({
    // do not externalize dependencies that need to be processed by webpack.
    // you can add more file types here
    // you should also whitelist deps that modifies `global` (e.g. polyfills)
    whitelist: /\.s?css$/,
  }),

  plugins: [
    new webpack.DefinePlugin({
      'process.env.REACT_ENV': '"server"',
      __SERVER__: true,
    }),
    // This is the plugin that turns the entire output of the server build
    // into a single JSON file. The default file name will be
    // `react-ssr-server-bundle.json`
    new ReactSSRServerPlugin(),
  ],
})

Client Config

import webpack from 'webpack'
import merge from 'webpack-merge'
// do not need 'html-webpack-plugin' any more because we will render html from server
// import HtmlWebpackPlugin from 'html-webpack-plugin'
import { ReactSSRClientPlugin } from 'react-server-renderer/client-plugin'

import { __DEV__, publicPath, resolve } from './config'

import base from './base'

export default merge.smart(base, {
  entry: {
    app: [resolve('src/entry-client.js')],
  },
  output: {
    publicPath,
    path: resolve('dist/static'),
    filename: `[name].[${__DEV__ ? 'hash' : 'chunkhash'}].js`,
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env.REACT_ENV': '"client"',
      __SERVER__: false,
    }),
    // This plugins generates `react-ssr-client-manifest.json` in the
    // output directory.
    new ReactSSRClientPlugin({
      // path relative to your output path, default to be `react-ssr-client-manifest.json`
      filename: '../react-ssr-client-manifest.json',
    }),
  ],
})

You can then use the generated client manifest, together with a page template:

import fs from 'node:fs'

import { createBundleRenderer } from 'react-server-renderer'

import serverBundle from '/path/to/react-ssr-server-bundle.json' with { type: 'json' }
import clientManifest from '/path/to/react-ssr-client-manifest.json' with { type: 'json' }

import template = fs.readFileSync('/path/to/template.html', 'utf-8')

const renderer = createBundleRenderer(serverBundle, {
  template,
  clientManifest,
})

With this setup, your server-rendered HTML for a build with code-splitting will look something like this (everything auto-injected):

<html>
  <head>
    <!-- chunks used for this render will be preloaded -->
    <link
      rel="preload"
      href="/manifest.js"
      as="script"
    />
    <link
      rel="preload"
      href="/main.js"
      as="script"
    />
    <link
      rel="preload"
      href="/0.js"
      as="script"
    />
    <!-- unused async chunks will be prefetched (lower priority) -->
    <link
      rel="prefetch"
      href="/1.js"
      as="script"
    />
  </head>
  <body>
    <!-- app content -->
    <div data-server-rendered="true"><div>async</div></div>
    <!-- manifest chunk should be first -->
    <script src="/manifest.js"></script>
    <!-- async chunks injected before main chunk -->
    <script src="/0.js"></script>
    <script src="/main.js"></script>
  </body>
</html>
`

Server bundle

All you need to do is for hot reload on development:

  1. compile server webpack config via node.js API like: const const serverCompiler = webpack(serverConfig)
  2. watch serverCompiler and replace server bundle on change

Example: https://github.com/JounQin/react-hackernews/blob/master/server/dev.js

Your server bundle entry should export a function with a context param which return a promise, and it should resolve a react component instance.

Example: https://github.com/JounQin/react-hackernews/blob/master/src/entry-server.js

When you need to redirect on server or an error occurs, you should reject inside promise so that we can handle it.

renderToString and renderToStream(use ReactDomServer.renderToNodeStream inside)

Since you generate server bundle renderer as above, you can easily call renderer.renderToString(context) or renderer.renderToStream(context), where context should be a singloton of every request.

renderToString is very simple, just try/catch error to handle it.

renderToStream is a tiny complicated to handle, you can rediect or reject request by listening error event and handle error param. If you want to render application but change response status, you can listen afterRender event and handle with your own context, for example maybe you want to render 404 Not Found page via React Component but respond with 404 status.

State management

If you set context.state on server, it will auto inject a script contains window.__INITIAL_STATE__ in output, so that you can resue server state on client.

Style injection and Head Management

Without SSR, we can easily use style-loader, however we need to collect rendered components with their styles together on runtime, so we choose to use react-style-loader which forked vue-style-loader indeed.

Let's create a simple HOC for server style, title management and http injection.

import axios from 'axios'
import hoistStatics from 'hoist-non-react-statics'
import PropTypes from 'prop-types'
import React from 'react'
import { withRouter } from 'react-router'

// custom dynamic title for better SEO both on server and client
const setTitle = (title, self) => {
  title = typeof title === 'function' ? title.call(self, self) : title

  if (!title) {
    return
  }

  if (__SERVER__) {
    self.props.staticContext.title = `React Server Renderer | ${title}`
  } else {
    // `title` here on client can be promise, but you should not and do not need to do that on server,
    // because on server async data will be fetched in asyncBootstrap first and set into store,
    // then title function will be called again when you call `renderToString` or `renderToStream`.
    // But on client, when you change route, maybe you need to fetch async data first
    // Example: https://github.com/JounQin/react-hackernews/blob/master/src/views/UserView/index.js#L18
    // And also, you need put `@withSsr` under `@connect` with `react-redux` for get store injected in your title function
    Promise.resolve(title).then(title => {
      if (title) {
        document.title = `React Server Renderer | ${title}`
      }
    })
  }
}

export const withSsr = (styles, router = true, title) => {
  if (typeof router !== 'boolean') {
    title = router
    router = true
  }

  return Component => {
    class SsrComponent extends React.PureComponent {
      static displayName = `Ssr${
        Component.displayName || Component.name || 'Component'
      }`

      static propTypes = {
        staticContext: PropTypes.object,
      }

      componentWillMount() {
        // `styles.__inject__` will only be exist on server, and inject into `staticContext`
        if (styles.__inject__) {
          styles.__inject__(this.props.staticContext)
        }

        setTitle(title, this)
      }

      render() {
        return (
          <Component
            {...this.props}
            // use different axios instance on server to handle different user client headers
            http={__SERVER__ ? this.props.staticContext.axios : axios}
          />
        )
      }
    }

    return hoistStatics(
      router ? withRouter(SsrComponent) : SsrComponent,
      Component,
    )
  }
}

Then use it:

import PropTypes from 'prop-types'
import React from 'react'
import { connect } from 'react-redux'

import { setCounter, increase, decrease } from 'store'
import { withSsr } from 'utils'

import styles from './styles'

@connect(
  ({ counter }) => ({ counter }),
  dispatch => ({
    setCounter: counter => dispatch(setCounter(counter)),
    increase: () => dispatch(increase),
    decrease: () => dispatch(decrease),
  }),
)
@withSsr(styles, false, ({ props }) => props.counter)
export default class Home extends React.PureComponent {
  static propTypes = {
    counter: PropTypes.number.isRequired,
    setCounter: PropTypes.func.isRequired,
    increase: PropTypes.func.isRequired,
    decrease: PropTypes.func.isRequired,
  }

  asyncBootstrap() {
    if (this.props.counter) {
      return true
    }

    return new Promise(resolve =>
      setTimeout(() => {
        this.props.setCounter(~~(Math.random() * 100))
        resolve(true)
      }, 500),
    )
  }

  render() {
    return (
      <div className="container">
        <h2 className={styles.heading}>Counter</h2>
        <button
          className="btn btn-primary"
          onClick={this.props.decrease}
        >
          -
        </button>
        {this.props.counter}
        <button
          className="btn btn-primary"
          onClick={this.props.increase}
        >
          +
        </button>
      </div>
    )
  }
}

And inside the template passed title to bundle renderer:

<html>
  <head>
    <title>{{ title }}</title>
  </head>
  <body>
    ...
  </body>
</html>

Then react-server-renderer will automatically collect user styles and title on server and render them into output!

Notes:

  • Use double-mustache (HTML-escaped interpolation) to avoid XSS attacks.
  • You should provide a default title when creating the context object in case no component has set a title during render.

Using the same strategy, you can easily expand it into a generic head management utility.


So actually it's not so simple right? Yes and no, if you choose to start using SSR, it is certain that you need pay for it, and after digging exist react SSR solutions like react-universally or any other, I find out Vue's solution is really great and simple.

Feature Request or Troubleshooting

Feel free to create an issue.

react-server-renderer's People

Contributors

greenkeeper[bot] avatar jounqin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

react-server-renderer's Issues

An in-range update of @types/react is breaking the build 🚨

The devDependency @types/react was updated from 16.4.14 to 16.4.15.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

@types/react is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ❌ continuous-integration/travis-ci/push: The Travis CI build failed (Details).

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

An in-range update of react is breaking the build 🚨

There have been updates to the react monorepo:

    • The devDependency react was updated from 16.6.1 to 16.6.2.
  • The devDependency react-dom was updated from 16.6.1 to 16.6.2.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

This monorepo update includes releases of one or more dependencies which all belong to the react group definition.

react is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ❌ continuous-integration/travis-ci/push: The Travis CI build could not complete due to an error (Details).

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

An in-range update of @types/react is breaking the build 🚨

The devDependency @types/react was updated from 16.4.16 to 16.4.17.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

@types/react is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ❌ continuous-integration/travis-ci/push: The Travis CI build failed (Details).

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

An in-range update of prettier is breaking the build 🚨

The devDependency prettier was updated from 1.14.2 to 1.14.3.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

prettier is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ❌ continuous-integration/travis-ci/push: The Travis CI build failed (Details).

Release Notes for 1.14.3

πŸ”— Changelog

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

An in-range update of react is breaking the build 🚨

There have been updates to the react monorepoundefined

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

This monorepo update includes releases of one or more dependencies which all belong to the react group definition.

react is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ❌ continuous-integration/travis-ci/push: The Travis CI build failed (Details).

Release Notes for v16.5.2

React DOM

Schedule (Experimental)

  • Renaming "tracking" API to "tracing" (@bvaughn in #13641)
  • Add UMD production+profiling entry points (@bvaughn in #13642)
  • Refactored schedule to remove some React-isms and improve performance for when deferred updates time out (@acdlite in #13582)
FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

An in-range update of @types/node is breaking the build 🚨

The devDependency @types/node was updated from 10.10.0 to 10.10.1.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

@types/node is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ❌ continuous-integration/travis-ci/push: The Travis CI build failed (Details).

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

An in-range update of @types/react-dom is breaking the build 🚨

The devDependency @types/react-dom was updated from 16.8.0 to 16.8.1.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

@types/react-dom is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ❌ continuous-integration/travis-ci/push: The Travis CI build failed (Details).

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.