{"id":28599889,"url":"https://github.com/un-ts/react-server-renderer","last_synced_at":"2025-08-23T12:37:46.741Z","repository":{"id":40348841,"uuid":"115746312","full_name":"un-ts/react-server-renderer","owner":"un-ts","description":"Yet another simple React SSR solution inspired by vue-server-render","archived":false,"fork":false,"pushed_at":"2024-01-07T15:41:22.000Z","size":3525,"stargazers_count":28,"open_issues_count":1,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-08-15T04:42:31.225Z","etag":null,"topics":["code-splitting","react","react-server-render","react-ssr","server-rendering"],"latest_commit_sha":null,"homepage":"https://github.com/JounQin/react-hackernews","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/un-ts.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null},"funding":{"github":["JounQin","1stG","rx-ts","un-ts"],"patreon":"1stG","open_collective":"unts","custom":["https://opencollective.com/1stG","https://opencollective.com/rxts","https://afdian.net/@JounQin"]}},"created_at":"2017-12-29T18:48:27.000Z","updated_at":"2024-01-06T11:33:46.000Z","dependencies_parsed_at":"2024-01-11T10:57:18.515Z","dependency_job_id":null,"html_url":"https://github.com/un-ts/react-server-renderer","commit_stats":{"total_commits":36,"total_committers":3,"mean_commits":12.0,"dds":"0.33333333333333337","last_synced_commit":"28697c06801898c9779e5b3b90b703cfd7c8e119"},"previous_names":["jounqin/react-server-renderer","jounqin/react-server-render"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/un-ts/react-server-renderer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/un-ts%2Freact-server-renderer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/un-ts%2Freact-server-renderer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/un-ts%2Freact-server-renderer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/un-ts%2Freact-server-renderer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/un-ts","download_url":"https://codeload.github.com/un-ts/react-server-renderer/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/un-ts%2Freact-server-renderer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271747028,"owners_count":24813606,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-23T02:00:09.327Z","response_time":69,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-splitting","react","react-server-render","react-ssr","server-rendering"],"created_at":"2025-06-11T13:38:52.602Z","updated_at":"2025-08-23T12:37:46.733Z","avatar_url":"https://github.com/un-ts.png","language":"TypeScript","funding_links":["https://github.com/sponsors/JounQin","https://github.com/sponsors/1stG","https://github.com/sponsors/rx-ts","https://github.com/sponsors/un-ts","https://patreon.com/1stG","https://opencollective.com/unts","https://opencollective.com/1stG","https://opencollective.com/rxts","https://afdian.net/@JounQin"],"categories":[],"sub_categories":[],"readme":"# react-server-renderer\n\n[![GitHub Actions](https://github.com/un-ts/react-server-renderer/workflows/CI/badge.svg)](https://github.com/un-ts/react-server-renderer/actions/workflows/ci.yml)\n[![npm](https://img.shields.io/npm/v/react-server-renderer.svg)](https://www.npmjs.com/package/react-server-renderer)\n[![GitHub Release](https://img.shields.io/github/release/un-ts/react-server-renderer)](https://github.com/un-ts/react-server-renderer/releases)\n\n[![Conventional Commits](https://img.shields.io/badge/conventional%20commits-1.0.0-yellow.svg)](https://conventionalcommits.org)\n[![Renovate enabled](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com)\n[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)\n[![Code Style: Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)\n[![changesets](https://img.shields.io/badge/maintained%20with-changesets-176de3.svg)](https://github.com/atlassian/changesets)\n\nYet another simple React SSR solution inspired by vue-server-render with:\n\n1. Server bundle with hot reload on development and source map support\n2. prefetch/preload client injection with ClientManifest, generated by webpack-plugin inside\n3. server css support with [react-style-loader](https://github.com/JounQin/react-style-loader)\n4. Async component support with [react-async-component](https://github.com/ctrlplusb/react-async-component) and [react-async-bootstrapper](https://github.com/ctrlplusb/react-async-bootstrapper)\n5. custom dynamic head management for better SEO\n\n## Real World Demo\n\n[react-hackernews](https://github.com/JounQin/react-hackernews)\n\n## Usage\n\nThis module is heavily inspired by [vue-server-render](https://ssr.vuejs.org), it is recommended to read about [bundle-renderer](https://ssr.vuejs.org/en/bundle-renderer.html).\n\nIt uses [react-router](https://github.com/remix-run/react-router) on server, so you should read about [Server Rendering](https://reactrouter.com/en/main/guides/ssr).\n\nAnd also, data injection should be implement with [asyncBootstrap](https://github.com/ctrlplusb/react-async-bootstrapper).\n\n### Build Configuration\n\n#### Server Config\n\n```js\nimport webpack from 'webpack'\nimport merge from 'webpack-merge'\nimport nodeExternals from 'webpack-node-externals'\nimport { ReactSSRServerPlugin } from 'react-server-renderer/server-plugin'\n\nimport { resolve } from './config'\n\nimport base from './base'\n\nexport default merge.smart(base, {\n  // Point entry to your app's server entry file\n  entry: resolve('src/entry-server.js'),\n\n  // This allows webpack to handle dynamic imports in a Node-appropriate\n  // fashion, and also tells `react-style-loader` to emit server-oriented code when\n  // compiling React components.\n  target: 'node',\n\n  output: {\n    path: resolve('dist'),\n    filename: `[name].[chunkhash].js`,\n    // This tells the server bundle to use Node-style exports\n    libraryTarget: 'commonjs2',\n  },\n\n  // https://webpack.js.org/configuration/externals/#function\n  // https://github.com/liady/webpack-node-externals\n  // Externalize app dependencies. This makes the server build much faster\n  // and generates a smaller bundle file.\n  externals: nodeExternals({\n    // do not externalize dependencies that need to be processed by webpack.\n    // you can add more file types here\n    // you should also whitelist deps that modifies `global` (e.g. polyfills)\n    whitelist: /\\.s?css$/,\n  }),\n\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env.REACT_ENV': '\"server\"',\n      __SERVER__: true,\n    }),\n    // This is the plugin that turns the entire output of the server build\n    // into a single JSON file. The default file name will be\n    // `react-ssr-server-bundle.json`\n    new ReactSSRServerPlugin(),\n  ],\n})\n```\n\n#### Client Config\n\n```js\nimport webpack from 'webpack'\nimport merge from 'webpack-merge'\n// do not need 'html-webpack-plugin' any more because we will render html from server\n// import HtmlWebpackPlugin from 'html-webpack-plugin'\nimport { ReactSSRClientPlugin } from 'react-server-renderer/client-plugin'\n\nimport { __DEV__, publicPath, resolve } from './config'\n\nimport base from './base'\n\nexport default merge.smart(base, {\n  entry: {\n    app: [resolve('src/entry-client.js')],\n  },\n  output: {\n    publicPath,\n    path: resolve('dist/static'),\n    filename: `[name].[${__DEV__ ? 'hash' : 'chunkhash'}].js`,\n  },\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env.REACT_ENV': '\"client\"',\n      __SERVER__: false,\n    }),\n    // This plugins generates `react-ssr-client-manifest.json` in the\n    // output directory.\n    new ReactSSRClientPlugin({\n      // path relative to your output path, default to be `react-ssr-client-manifest.json`\n      filename: '../react-ssr-client-manifest.json',\n    }),\n  ],\n})\n```\n\nYou can then use the generated client manifest, together with a page template:\n\n```js\nimport fs from 'node:fs'\n\nimport { createBundleRenderer } from 'react-server-renderer'\n\nimport serverBundle from '/path/to/react-ssr-server-bundle.json' with { type: 'json' }\nimport clientManifest from '/path/to/react-ssr-client-manifest.json' with { type: 'json' }\n\nimport template = fs.readFileSync('/path/to/template.html', 'utf-8')\n\nconst renderer = createBundleRenderer(serverBundle, {\n  template,\n  clientManifest,\n})\n```\n\nWith this setup, your server-rendered HTML for a build with code-splitting will look something like this (everything auto-injected):\n\n```html\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003c!-- chunks used for this render will be preloaded --\u003e\n    \u003clink\n      rel=\"preload\"\n      href=\"/manifest.js\"\n      as=\"script\"\n    /\u003e\n    \u003clink\n      rel=\"preload\"\n      href=\"/main.js\"\n      as=\"script\"\n    /\u003e\n    \u003clink\n      rel=\"preload\"\n      href=\"/0.js\"\n      as=\"script\"\n    /\u003e\n    \u003c!-- unused async chunks will be prefetched (lower priority) --\u003e\n    \u003clink\n      rel=\"prefetch\"\n      href=\"/1.js\"\n      as=\"script\"\n    /\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003c!-- app content --\u003e\n    \u003cdiv data-server-rendered=\"true\"\u003e\u003cdiv\u003easync\u003c/div\u003e\u003c/div\u003e\n    \u003c!-- manifest chunk should be first --\u003e\n    \u003cscript src=\"/manifest.js\"\u003e\u003c/script\u003e\n    \u003c!-- async chunks injected before main chunk --\u003e\n    \u003cscript src=\"/0.js\"\u003e\u003c/script\u003e\n    \u003cscript src=\"/main.js\"\u003e\u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n`\n```\n\n### Server bundle\n\nAll you need to do is for hot reload on development:\n\n1. compile server webpack config via node.js API like: `const const serverCompiler = webpack(serverConfig)`\n2. watch serverCompiler and replace server bundle on change\n\nExample: https://github.com/JounQin/react-hackernews/blob/master/server/dev.js\n\nYour server bundle entry should export a function with a `context` param which return a promise, and it should resolve a react component instance.\n\nExample: https://github.com/JounQin/react-hackernews/blob/master/src/entry-server.js\n\nWhen you need to redirect on server or an error occurs, you should reject inside promise so that we can handle it.\n\n### renderToString and renderToStream(use `ReactDomServer.renderToNodeStream` inside)\n\nSince 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.\n\n`renderToString` is very simple, just `try/catch` error to handle it.\n\n`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.\n\n### State management\n\nIf 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.\n\n### Style injection and Head Management\n\nWithout 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](https://github.com/JounQin/react-style-loader) which forked [vue-style-loader](https://github.com/vuejs/vue-style-loader) indeed.\n\nLet's create a simple HOC for server style, title management and http injection.\n\n```js\nimport axios from 'axios'\nimport hoistStatics from 'hoist-non-react-statics'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { withRouter } from 'react-router'\n\n// custom dynamic title for better SEO both on server and client\nconst setTitle = (title, self) =\u003e {\n  title = typeof title === 'function' ? title.call(self, self) : title\n\n  if (!title) {\n    return\n  }\n\n  if (__SERVER__) {\n    self.props.staticContext.title = `React Server Renderer | ${title}`\n  } else {\n    // `title` here on client can be promise, but you should not and do not need to do that on server,\n    // because on server async data will be fetched in asyncBootstrap first and set into store,\n    // then title function will be called again when you call `renderToString` or `renderToStream`.\n    // But on client, when you change route, maybe you need to fetch async data first\n    // Example: https://github.com/JounQin/react-hackernews/blob/master/src/views/UserView/index.js#L18\n    // And also, you need put `@withSsr` under `@connect` with `react-redux` for get store injected in your title function\n    Promise.resolve(title).then(title =\u003e {\n      if (title) {\n        document.title = `React Server Renderer | ${title}`\n      }\n    })\n  }\n}\n\nexport const withSsr = (styles, router = true, title) =\u003e {\n  if (typeof router !== 'boolean') {\n    title = router\n    router = true\n  }\n\n  return Component =\u003e {\n    class SsrComponent extends React.PureComponent {\n      static displayName = `Ssr${\n        Component.displayName || Component.name || 'Component'\n      }`\n\n      static propTypes = {\n        staticContext: PropTypes.object,\n      }\n\n      componentWillMount() {\n        // `styles.__inject__` will only be exist on server, and inject into `staticContext`\n        if (styles.__inject__) {\n          styles.__inject__(this.props.staticContext)\n        }\n\n        setTitle(title, this)\n      }\n\n      render() {\n        return (\n          \u003cComponent\n            {...this.props}\n            // use different axios instance on server to handle different user client headers\n            http={__SERVER__ ? this.props.staticContext.axios : axios}\n          /\u003e\n        )\n      }\n    }\n\n    return hoistStatics(\n      router ? withRouter(SsrComponent) : SsrComponent,\n      Component,\n    )\n  }\n}\n```\n\nThen use it:\n\n```js\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { connect } from 'react-redux'\n\nimport { setCounter, increase, decrease } from 'store'\nimport { withSsr } from 'utils'\n\nimport styles from './styles'\n\n@connect(\n  ({ counter }) =\u003e ({ counter }),\n  dispatch =\u003e ({\n    setCounter: counter =\u003e dispatch(setCounter(counter)),\n    increase: () =\u003e dispatch(increase),\n    decrease: () =\u003e dispatch(decrease),\n  }),\n)\n@withSsr(styles, false, ({ props }) =\u003e props.counter)\nexport default class Home extends React.PureComponent {\n  static propTypes = {\n    counter: PropTypes.number.isRequired,\n    setCounter: PropTypes.func.isRequired,\n    increase: PropTypes.func.isRequired,\n    decrease: PropTypes.func.isRequired,\n  }\n\n  asyncBootstrap() {\n    if (this.props.counter) {\n      return true\n    }\n\n    return new Promise(resolve =\u003e\n      setTimeout(() =\u003e {\n        this.props.setCounter(~~(Math.random() * 100))\n        resolve(true)\n      }, 500),\n    )\n  }\n\n  render() {\n    return (\n      \u003cdiv className=\"container\"\u003e\n        \u003ch2 className={styles.heading}\u003eCounter\u003c/h2\u003e\n        \u003cbutton\n          className=\"btn btn-primary\"\n          onClick={this.props.decrease}\n        \u003e\n          -\n        \u003c/button\u003e\n        {this.props.counter}\n        \u003cbutton\n          className=\"btn btn-primary\"\n          onClick={this.props.increase}\n        \u003e\n          +\n        \u003c/button\u003e\n      \u003c/div\u003e\n    )\n  }\n}\n```\n\nAnd inside the template passed `title` to bundle renderer:\n\n```html\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003ctitle\u003e{{ title }}\u003c/title\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    ...\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\nThen `react-server-renderer` will automatically collect user styles and title on server and render them into output!\n\nNotes:\n\n- Use double-mustache (HTML-escaped interpolation) to avoid XSS attacks.\n- You should provide a default title when creating the context object in case no component has set a title during render.\n\nUsing the same strategy, you can easily expand it into a generic head management utility.\n\n---\n\nSo 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](https://github.com/ctrlplusb/react-universally) or any other, I find out Vue's solution is really great and simple.\n\n## Feature Request or Troubleshooting\n\nFeel free to [create an issue](https://github.com/JounQin/react-server-renderer/issues/new).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fun-ts%2Freact-server-renderer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fun-ts%2Freact-server-renderer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fun-ts%2Freact-server-renderer/lists"}