react-async-ssr
Advanced tools
Comparing version 0.0.0 to 0.1.0
# Changelog | ||
## 0.1.0 | ||
* Initial release |
@@ -7,3 +7,3 @@ /* -------------------- | ||
// Exports | ||
module.exports = {}; | ||
// Re-export lib | ||
module.exports = require('./lib/'); |
{ | ||
"name": "react-async-ssr", | ||
"version": "0.0.0", | ||
"version": "0.1.0", | ||
"description": "Render React Suspense on server", | ||
@@ -17,5 +17,17 @@ "main": "index.js", | ||
"dependencies": { | ||
"react-dom": "^16.7.0" | ||
}, | ||
"peerDependencies": { | ||
"react": "^16.7.0" | ||
}, | ||
"devDependencies": { | ||
"@babel/cli": "^7.2.3", | ||
"@babel/core": "^7.2.2", | ||
"@babel/preset-env": "^7.2.3", | ||
"@babel/preset-react": "^7.0.0", | ||
"@overlookmotel/eslint-config": "^1.1.0", | ||
"@overlookmotel/eslint-config-react": "^1.0.0", | ||
"@overlookmotel/eslint-config-tests": "^1.0.2", | ||
"chai": "^4.2.0", | ||
"chai-as-promised": "^7.1.1", | ||
"coveralls": "^3.0.2", | ||
@@ -25,4 +37,11 @@ "cross-env": "^5.2.0", | ||
"eslint-plugin-chai-friendly": "^0.4.1", | ||
"eslint-plugin-eslint-comments": "^3.0.1", | ||
"eslint-plugin-react": "^7.12.4", | ||
"istanbul": "^0.4.5", | ||
"mocha": "^5.2.0" | ||
"mocha": "^5.2.0", | ||
"prop-types": "^15.6.2", | ||
"react": "^16.7.0", | ||
"sinon": "^7.2.3", | ||
"sinon-chai": "^3.3.0", | ||
"source-map-support": "^0.5.10" | ||
}, | ||
@@ -39,9 +58,15 @@ "keywords": [ | ||
"scripts": { | ||
"build": "rm -rf test/* && npm run build-only", | ||
"build-only": "babel src --out-dir .", | ||
"test": "npm run lint && npm run test-main", | ||
"lint": "eslint \"**/*.js\" --ext .js", | ||
"test-main": "mocha --check-leaks --colors -t 1000 -R spec \"test/**/*.test.js\"", | ||
"lint": "eslint *.js .*.js lib/** src/** --ext .js", | ||
"lint-fix": "eslint *.js .*.js lib/** src/** --ext .js --fix", | ||
"test-main": "npm run build && npm run test-only && npm run test-only-prod", | ||
"test-only": "cross-env NODE_ENV=development mocha --require source-map-support/register --check-leaks --colors -t 1000 -R spec 'test/**/*.test.js'", | ||
"test-only-prod": "cross-env NODE_ENV=production mocha --require source-map-support/register --check-leaks --colors -t 1000 -R spec 'test/**/*.test.js'", | ||
"cover": "npm run cover-main && rm -rf coverage", | ||
"coveralls": "npm run cover-main && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage", | ||
"cover-main": "cross-env COVERAGE=true istanbul cover _mocha --report lcovonly -- -t 1000 -R spec \"test/**/*.test.js\"", | ||
"travis": "if [ $COVERAGE ]; then npm run coveralls; else npm test; fi" | ||
"cover-main": "npm run build && npm run cover-only", | ||
"cover-only": "cross-env COVERAGE=true istanbul cover _mocha --report lcovonly -- -t 1000 -R spec 'test/**/*.test.js'", | ||
"ci": "if [ $COVERAGE ]; then npm run coveralls; else npm test; fi" | ||
}, | ||
@@ -48,0 +73,0 @@ "engines": { |
192
README.md
@@ -1,5 +0,1 @@ | ||
# Render React Suspense on server | ||
## Current status | ||
[![NPM version](https://img.shields.io/npm/v/react-async-ssr.svg)](https://www.npmjs.com/package/react-async-ssr) | ||
@@ -12,6 +8,191 @@ [![Build Status](https://img.shields.io/travis/overlookmotel/react-async-ssr/master.svg)](http://travis-ci.org/overlookmotel/react-async-ssr) | ||
# Render React Suspense on server | ||
[React](http://www.reactjs.org/) v16.6.0 introduced [Suspense](https://reactjs.org/docs/code-splitting.html#suspense) for lazy-loading components, but it doesn't work yet on the server-side. | ||
This package enables server-side rendering. | ||
It provides async versions of `.renderToString()` and `.renderToStaticMarkup()` methods. The async methods support `Suspense` and allow async loading of components or data. | ||
## Usage | ||
This module is under development and not ready for use yet. | ||
### Installation | ||
``` | ||
npm install react-async-ssr | ||
``` | ||
### Moving to async server-side rendering | ||
Before: | ||
```js | ||
const ReactDOM = require('react-dom/server'); | ||
function render() { | ||
const html = ReactDOM.renderToString(<App />); | ||
return html; | ||
} | ||
``` | ||
After: | ||
```js | ||
const ReactDOM = require('react-async-ssr'); | ||
async function render() { | ||
const html = await ReactDOM.renderToStringAsync(<App />); | ||
return html; | ||
} | ||
``` | ||
### Application code | ||
```js | ||
function App() { | ||
return ( | ||
<div> | ||
<Suspense fallback={<Spinner />}> | ||
<LazyComponent /> | ||
<LazyComponent /> | ||
<LazyData /> | ||
</Suspense> | ||
</div> | ||
); | ||
} | ||
const html = await ReactDOM.renderToStringAsync(<App />); | ||
``` | ||
`<Suspense>` behaves exactly the same on the server as it does on the client. | ||
`.renderToStringAsync()` will render the app in the usual way, except any lazy elements will be awaited and rendered before the returned promise resolves. | ||
### Lazy components | ||
So I can just use `React.lazy()`, right? | ||
No! `React.lazy()` doesn't make sense to use on the server side. It doesn't have any ability to track the modules that have been lazy-loaded, so there's no way to then reload them on the client side, so that `.hydrate()` has all the code it needs. | ||
[@loadable/component](https://www.smooth-code.com/open-source/loadable-components/docs/api-loadable-component/#lazy) provides a `.lazy()` method which is equivalent to `React.lazy()` but suitable for server-side rendering. [This guide](https://www.smooth-code.com/open-source/loadable-components/docs/server-side-rendering/) explains how to use it for server-side rendering. | ||
### Lazy data | ||
`.renderToStringAsync()` supports any component which fits within React's convention for suspendable components. | ||
In it's `render()` method, the component should throw a Promise which will resolve when the data is loaded. When the promise resolves, the renderer will re-render the component and add it into the markup. | ||
#### Basic example | ||
```js | ||
let data = null; | ||
function LazyData() { | ||
if (!data) { | ||
const promise = new Promise( | ||
resolve => setTimeout(() => { | ||
data = {foo: 'bar'}; | ||
resolve(); | ||
}, 1000) | ||
); | ||
throw promise; | ||
} | ||
return <div>{data.foo}</div>; | ||
} | ||
function App() { | ||
return ( | ||
<div> | ||
<Suspense fallback={<div>Loading...</div>}> | ||
<LazyData /> | ||
</Suspense> | ||
</div> | ||
); | ||
} | ||
``` | ||
#### react-cache example | ||
An example using the experimental package [react-cache](https://www.npmjs.com/package/react-cache): | ||
```js | ||
const {createResource} = require('react-cache'); | ||
const PokemonResource = createResource( | ||
id => | ||
fetch(`https://pokeapi.co/api/v2/pokemon/${id}/`) | ||
.then(res => res.json()) | ||
); | ||
function Pokemon(props) { | ||
const data = PokemonResource.read(props.id); | ||
return <div>My name is {data.name}</div>; | ||
} | ||
function App() { | ||
return ( | ||
<div> | ||
<Suspense fallback={<div>Loading...</div>}> | ||
<Pokemon id={1} /> | ||
<Pokemon id={2} /> | ||
<Pokemon id={3} /> | ||
</Suspense> | ||
</div> | ||
); | ||
} | ||
const html = await ReactDOM.renderToStringAsync(<App />); | ||
``` | ||
The above example makes 3 async fetch requests, which are made in parallel. They are awaited, and the HTML markup rendered only once all the data is ready. | ||
#### Complicated cases | ||
`.renderToStringAsync()` supports: | ||
* Async components which themselves load more async components/data | ||
* Suspense fallbacks which load async components/data | ||
#### Hydrating the render on client side | ||
The classic model for SSR is: | ||
* Server-side: Load all data required for page asynchronously | ||
* Server-side: Pass data into React app and render synchronously | ||
* Send to client: Rendered HTML + data as JSON | ||
* Client-side: Browser renders static HTML initially | ||
* Client-side: Once all components and data required are loaded, "hydrate" the page using `ReactDOM.hydrate()` | ||
With async SSR, the server-side process is different: | ||
* Server-side: Asynchronously render React app | ||
* Server-side: Data loaded asynchronously *during* render | ||
* (remaining steps same as above) | ||
The advantages of this change are: | ||
* Components define their own data needs | ||
* No need for data dependencies to be "hoisted" to the page's root component | ||
* Therefore, components are less tightly coupled (the React way!) | ||
In the example above, the `<Pokemon>` component is completely independent. You can drop it in to any app, anywhere in the component tree, and it'll be able to load the data it needs, without any complex "wiring" up. | ||
However, some mechanism is required to gather the data loaded on the server in order to send it to the client for hydration. | ||
There are many solutions, for example using a [Redux](https://redux.js.org/) store, or a [Context](https://reactjs.org/docs/context.html) Provider at the root of the app. This package does not make any assumptions about how the user wants to handle this, and no doubt solutions will emerge from the community. All that this package requires is that components follow React's convention that components wishing to do async loading throw promises. | ||
### Additional notes | ||
#### Stream rendering | ||
Stream rendering (`.renderToNodeStream()`) is not yet supported by this package. | ||
#### No double-rendering | ||
Many other solutions achieve something like this by "double-rendering" the app. | ||
In the first render pass, all the promises for async-loaded data are collected. Once all the promises resolve, a 2nd render pass produces the actual HTML markup which is sent to the client. Obviously, this is resource-intensive. And if async components themselves make further async requests, 3rd or 4th or more render passes can be required. | ||
The `.renderToStringAsync()` method provided by this package renders in a single pass. The render is interrupted when awaiting an async resource and resumed once it has loaded. | ||
## Tests | ||
@@ -36,1 +217,2 @@ | ||
* document new functionality/API additions in README | ||
* do not add an entry to Changelog (Changelog is created when cutting releases) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Trivial Package
Supply chain riskPackages less than 10 lines of code are easily copied into your own project and may not warrant the additional supply chain risk of an external dependency.
Found 1 instance in 1 package
33823
14
697
217
2
22
+ Addedreact-dom@^16.7.0
+ Addedjs-tokens@4.0.0(transitive)
+ Addedloose-envify@1.4.0(transitive)
+ Addedobject-assign@4.1.1(transitive)
+ Addedprop-types@15.8.1(transitive)
+ Addedreact@16.14.0(transitive)
+ Addedreact-dom@16.14.0(transitive)
+ Addedreact-is@16.13.1(transitive)
+ Addedscheduler@0.19.1(transitive)