• Stars
    star
    5,049
  • Rank 8,246 (Top 0.2 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created about 7 years ago
  • Updated 9 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

👻 Zero-configuration framework-agnostic static prerendering for SPAs

Stand With Ukraine

react-snap Build Status npm npm Twitter Follow

Pre-renders a web app into static HTML. Uses Headless Chrome to crawl all available links starting from the root. Heavily inspired by prep and react-snapshot, but written from scratch. Uses best practices to get the best loading performance.

😍 Features

  • Enables SEO (Google, DuckDuckGo...) and SMO (Twitter, Facebook...) for SPAs.
  • Works out-of-the-box with create-react-app - no code-changes required.
  • Uses a real browser behind the scenes, so there are no issues with unsupported HTML5 features, like WebGL or Blobs.
  • Does a lot of load performance optimization. Here are details, if you are curious.
  • Does not depend on React. The name is inspired by react-snapshot but works with any technology (e.g., Vue).
  • npm package does not have a compilation step, so you can fork it, change what you need, and install it with a GitHub URL.

Zero configuration is the main feature. You do not need to worry about how it works or how to configure it. But if you are curious, here are details.

Basic usage with create-react-app

Install:

yarn add --dev react-snap

Change package.json:

"scripts": {
  "postbuild": "react-snap"
}

Change src/index.js (for React 16+):

import { hydrate, render } from "react-dom";

const rootElement = document.getElementById("root");
if (rootElement.hasChildNodes()) {
  hydrate(<App />, rootElement);
} else {
  render(<App />, rootElement);
}

That's it!

Basic usage with Preact

To do hydration in Preact you need to use this trick:

const rootElement = document.getElementById("root");
if (rootElement.hasChildNodes()) {
  preact.render(<App />, rootElement, rootElement.firstElementChild);
} else {
  preact.render(<App />, rootElement);
}

Basic usage with Vue.js

Install:

yarn add --dev react-snap

Change package.json:

"scripts": {
  "postbuild": "react-snap"
},
"reactSnap": {
  "source": "dist",
  "minifyHtml": {
    "collapseWhitespace": false,
    "removeComments": false
  }
}

Or use preserveWhitespace: false in vue-loader.

source - output folder of webpack or any other bundler of your choice

Read more about minifyHtml caveats in #142.

Example: Switch from prerender-spa-plugin to react-snap

Caveats

Only works with routing strategies using the HTML5 history API. No hash(bang) URLs.

Vue uses the data-server-rendered attribute on the root element to mark SSR generated markup. When this attribute is present, the VDOM rehydrates instead of rendering everything from scratch, which can result in a flash.

This is a small hack to fix rehydration problem:

window.snapSaveState = () => {
  document.querySelector("#app").setAttribute("data-server-rendered", "true");
};

window.snapSaveState is a callback to save the state of the application at the end of rendering. It can be used for Redux or async components. In this example, it is repurposed to alter the DOM, this is why I call it a "hack." Maybe in future versions of react-snap, I will come up with better abstractions or automate this process.

Vue 1.x

Make sure to use replace: false for root components

Examples

⚙️ Customization

If you need to pass some options for react-snap, you can do this in your package.json like this:

"reactSnap": {
  "inlineCss": true
}

Not all options are documented yet, but you can check defaultOptions in index.js.

inlineCss

Experimental feature - requires improvements.

react-snap can inline critical CSS with the help of minimalcss and full CSS will be loaded in a non-blocking manner with the help of loadCss.

Use inlineCss: true to enable this feature.

TODO: as soon as this feature is stable, it should be enabled by default.

⚠️ Caveats

Async components

Also known as code splitting, dynamic import (TC39 proposal), "chunks" (which are loaded on demand), "layers", "rollups", or "fragments". See: Guide To JavaScript Async Components

An async component (in React) is a technique (typically implemented as a higher-order component) for loading components on demand with the dynamic import operator. There are a lot of solutions in this field. Here are some examples:

It is not a problem to render async components with react-snap, the tricky part happens when a prerendered React application boots and async components are not loaded yet, so React draws the "loading" state of a component, and later when the component is loaded, React draws the actual component. As a result, the user sees a flash:

100%                    /----|    |----
                       /     |    |
                      /      |    |
                     /       |    |
                    /        |____|
  visual progress  /
                  /
0%  -------------/

Usually a code splitting library provides an API to handle it during SSR, but as long as "real" SSR is not used in react-snap - the issue surfaces, and there is no simple way to fix it.

  1. Use react-prerendered-component. This library holds onto the prerendered HTML until the dynamically imported code is ready.
import loadable from "@loadable/component";
import { PrerenderedComponent } from "react-prerendered-component";

const prerenderedLoadable = dynamicImport => {
  const LoadableComponent = loadable(dynamicImport);
  return React.memo(props => (
    // you can use the `.preload()` method from react-loadable or react-imported-component`
    <PrerenderedComponent live={LoadableComponent.load()}>
      <LoadableComponent {...props} />
    </PrerenderedComponent>
  ));
};

const MyComponent = prerenderedLoadable(() => import("./MyComponent"));

MyComponent will use prerendered HTML to prevent the page content from flashing (it will find the required piece of HTML using an id attribute generated by PrerenderedComponent and inject it using dangerouslySetInnerHTML).

  1. The same approach will work with React.lazy, but React.lazy doesn't provide a prefetch method (load or preload), so you need to implement it yourself (this can be a fragile solution).
const prefetchMap = new WeakMap();
const prefetchLazy = LazyComponent => {
  if (!prefetchMap.has(LazyComponent)) {
    prefetchMap.set(LazyComponent, LazyComponent._ctor());
  }
  return prefetchMap.get(LazyComponent);
};

const prerenderedLazy = dynamicImport => {
  const LazyComponent = React.lazy(dynamicImport);
  return React.memo(props => (
    <PrerenderedComponent live={prefetchLazy(LazyComponent)}>
      <LazyComponent {...props} />
    </PrerenderedComponent>
  ));
};

const MyComponent = prerenderedLazy(() => import("./MyComponent"));
  1. use loadable-components 2.2.3 (current is >5). The old version of loadable-components can solve this issue for a "snapshot" setup:
import { loadComponents, getState } from "loadable-components";
window.snapSaveState = () => getState();

loadComponents()
  .then(() => hydrate(AppWithRouter, rootElement))
  .catch(() => render(AppWithRouter, rootElement));

If you don't use babel plugin, don't forget to provide modules:

const NotFoundPage = loadable(() => import("src/pages/NotFoundPage"), {
  modules: ["NotFoundPage"]
});

loadable-components were deprecated in favour of @loadable/component, but @loadable/component dropped getState. So if you want to use loadable-components you can use old version (2.2.3 latest version at the moment of writing) or you can wait until React will implement proper handling of this case with asynchronous rendering and React.lazy.

Redux

See: Redux Server Rendering Section

// Grab the state from a global variable injected into the server-generated HTML
const preloadedState = window.__PRELOADED_STATE__;

// Allow the passed state to be garbage-collected
delete window.__PRELOADED_STATE__;

// Create Redux store with initial state
const store = createStore(counterApp, preloadedState || initialState);

// Tell react-snap how to save Redux state
window.snapSaveState = () => ({
  __PRELOADED_STATE__: store.getState()
});

Caution: as of now, only basic "JSON" data types are supported: e.g. Date, Set, Map, and NaN won't be handled correctly (#54).

Third-party requests: Google Analytics, Mapbox, etc.

You can block all third-party requests with the following config:

"skipThirdPartyRequests": true

AJAX

react-snap can capture all AJAX requests. It will store json requests in the domain in window.snapStore[<path>], where <path> is the path of the request.

Use "cacheAjaxRequests": true to enable this feature.

This feature can conflict with the browser cache. See #197 for details. You may want to disable cache in this case: "puppeteer": { "cache": false }.

Service Workers

By default, create-react-app uses index.html as a fallback:

navigateFallback: publicUrl + '/index.html',

You need to change this to an un-prerendered version of index.html - 200.html, otherwise you will see index.html flash on other pages (if you have any). See Configure sw-precache without ejecting for more information.

Containers and other restricted environments

Puppeteer (Headless Chrome) may fail due to sandboxing issues. To get around this, you may use:

"puppeteerArgs": ["--no-sandbox", "--disable-setuid-sandbox"]

Read more about puppeteer troubleshooting.

"inlineCss": true sometimes causes problems in containers.

Docker + Alpine

To run react-snap inside docker with Alpine, you might want to use a custom Chromium executable. See #93 and #132.

Heroku

heroku buildpacks:add https://github.com/jontewks/puppeteer-heroku-buildpack.git
heroku buildpacks:add heroku/nodejs
heroku buildpacks:add https://github.com/heroku/heroku-buildpack-static.git

See this PR. At the moment of writing, Heroku doesn't support HTTP/2.

Semantic UI

Semantic UI is defined over class substrings that contain spaces (e.g., "three column"). Sorting the class names, therefore, breaks the styling. To get around this, use the following configuration:

"minifyHtml": { "sortClassName": false }

From version 1.17.0, sortClassName is false by default.

JSS

Once JS on the client is loaded, components initialized and your JSS styles are regenerated, it's a good time to remove server-side generated style tag in order to avoid side-effects

https://github.com/cssinjs/jss/blob/master/docs/ssr.md

This basically means that JSS doesn't support rehydration. See #99 for a possible solutions.

react-router v3

See #135.

userAgent

You can use navigator.userAgent == "ReactSnap" to do some checks in the app code while snapping—for example, if you use an absolute path for your API AJAX request. While crawling, however, you should request a specific host.

Example code:

const BASE_URL =
  process.env.NODE_ENV == "production" && navigator.userAgent != "ReactSnap"
    ? "/"
    : "http://xxx.yy/rest-api";

Alternatives

See alternatives.

Who uses it

cloud.gov.au blacklane reformma

Contributing

Report a bug

Please provide a reproducible demo of a bug and steps to reproduce it. Thanks!

Share on the web

Tweet it, like it, share it, star it. Thank you.

Code

You can also contribute to minimalcss, which is a big part of react-snap. Also, give it some stars.

More Repositories

1

react-ideal-image

🖼️ An Almost Ideal React Image Component
JavaScript
3,273
star
2

package.json

🗃️ package.json fields explained
951
star
3

css-in-js-101

💈 CSS-in-JS 101: All you need to know
249
star
4

type-o-rama

👾 JS type systems interportability
243
star
5

programming-languages-genealogical-tree

Programming languages genealogical tree
231
star
6

typescript-monorepo

HTML
153
star
7

jekyll-press

🚨 [deprecated] Minifier plugin for jekyll. Minifies all html, js, css files. Simple just drop it in solution. No Java required
Ruby
149
star
8

react-modal-experiment

JavaScript
109
star
9

write-you-a-programming-language

List of small programming languages that you can implement in a relatively small amount of time for educational purposes.
99
star
10

guide-to-async-components

📖 Guide To JavaScript Async Components
79
star
11

diamondback-ruby

⛑ Fork of diamondback-ruby
Ruby
70
star
12

awesome-hiring-process

Collection of links and ideas about the hiring process in the IT industry
58
star
13

ruby-json-benchmark

Ruby json benchmark
Ruby
36
star
14

html_press

🚨 [deprecated] Ruby gem for compressing html
Ruby
35
star
15

pragmatic-types

Small practical guide on Flow and TypeScript for JavaScript developers
33
star
16

jshintrb

🚨 [deprecated] Ruby wrapper for JSHint
Ruby
31
star
17

mobile-safari-fullscreen

Fix for Mobile Safari fullscreen issue
JavaScript
23
star
18

ruby-memory-issues

🐲 Ruby Memory Issues and Where to Find Them
22
star
19

the-button

JavaScript
17
star
20

react-accessible-accordion

Accessible React accordion component
JavaScript
15
star
21

awesome-vscode-dev-containers

A curated list of VS Code Containers
15
star
22

rb-fchange

🚨 [deprecated] Gem which uses native windows methods for watching changes of file system
Ruby
15
star
23

d3-tube

D3 implementation of tube map
JavaScript
14
star
24

main-module-browser-test

Experiment
JavaScript
12
star
25

jekyll_oembed

🚨 [deprecated] Jekyll plugin to embed objects with the help of oEmbed. Simple liquid tag
Ruby
12
star
26

html_minifier

Ruby wrapper for kangax html-minifier
JavaScript
11
star
27

dev.wtf

HTML
10
star
28

headless-cms-comparison

JavaScript
9
star
29

the-history-of-frontend-development

Current state of frontend development is controversial. How we got here?
9
star
30

rb-notifu

🚨 [deprecated] Notification system for windows. Trying to be Growl
Ruby
8
star
31

social_detector

Detect if visitors are logged into social networks
Ruby
8
star
32

sqip.macro

Webpack sqip-loader implemented as babel-plugin-macros
JavaScript
8
star
33

webpack-comment-import.macro

JavaScript
7
star
34

css-modules-components

styled-components for CSS Modules
JavaScript
7
star
35

postgresql-experiment

JavaScript
6
star
36

css_press

🚨 [deprecated] Ruby gem for compressing CSS
Ruby
6
star
37

jBar2

jBar is a simple and lightweight jQuery notification (call to action) banner
JavaScript
4
star
38

useful-react-snippets

TypeScript
4
star
39

tree-sitter-wasm

Shell
4
star
40

react-async-issue

JavaScript
3
star
41

unstyled-components

styled-components based on React's style prop
JavaScript
3
star
42

react-lingui-example

JavaScript
3
star
43

github-issues

Github issues
3
star
44

react-fsm-example

JavaScript
3
star
45

react-simple-country-select

JavaScript
3
star
46

submodule

Small gem to simplify building process of gems with git submodules. Tended to be used for ruby gems which wrap js libraries or another assets
Ruby
2
star
47

zoos

Zoo is an attempt to classify things.
2
star
48

stereobooster.github.io

CSS
2
star
49

wisp

Mirror of https://hg.sr.ht/~arnebab/wisp
Scheme
2
star
50

readme

About me and my open-source work
2
star
51

parsing-with-derivalives

JavaScript
2
star
52

cs-video

Computer science videos
2
star
53

puma-benchmarks

Ruby
2
star
54

small-bits

Small bits of web UX
JavaScript
1
star
55

docker-reasonml

Dockerfile
1
star
56

waypoint-bug-report

waypoint-bug-report
JavaScript
1
star
57

react-router-redux-example

JavaScript
1
star
58

natural_sort-pgxn

Makefile
1
star
59

art_typograf

Ruby wrapper for typograf.artlebedev.ru webservice
Ruby
1
star
60

crystal-lisp

Crystal
1
star
61

main-module-browser

1
star
62

explain-you-mysql

JavaScript
1
star
63

loadable-components.macro

JavaScript
1
star
64

jekyll-seo-lint

Jekyll plugin to validate metadata provided in front matter and in configuration
Ruby
1
star
65

jevix

mirror of http://jevix.googlecode.com/svn/
PHP
1
star
66

jekyll_press

moved to https://github.com/stereobooster/jekyll-press
Ruby
1
star
67

react-ts-experiment

TypeScript
1
star