Package Exports
- coren
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (coren) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Coren
React Pluggable Serverside Render
Is serverside render a big headache for your Single Page App?
say you need head title, description, jsonld, og...
perhaps fetch data from db, then render redux preloadedState
so many things need to be rendered in HTML
How about we use more flexible way to solve it?
What if we let component define what they need in static method?
What if we could fetch database in component?
Coren provide you pluggable, flexible way to render your html
Table Of Content
Features
- 🔌Pluggable: You can customize your own collector for your own need
- Access to Component Props: in componentDidConstruct method from Lifecycle Hook you can access to component props
- Pass Variables To Component: Collector can pass anything you want(
DB Query,Server API) to Define method
Installation
$ npm install coren --saveSimple Example
Here's some simple example components using collector
Head
we'll insert what component want as
React
@collector()
export default class User extends Component {
// Put `user ${props.userId}` title tag to HTML
static defineHead(props) {
return {
title: `user ${props.userId}`
}
}
render() {
return <div>
...
</div>;
}
}How HeadCollector insert head
In MultiRoutesRenderer, HeadCollector insert head using appendToHead
class HeadCollector {
constructor() {
this.heads = [];
}
// ...
componentDidConstruct(id, component, props) {
this.heads.push(component.defineHead(props));
}
getFirstHead() {
return this.heads[0] || {};
}
// ...
appendToHead($head) {
const {title, description} = this.getFirstHead();
$head.append(`<title>${title}</title>`);
$head.append(`<meta name="description" content="${description}">`);
}
}Serverside
const app = new App({
path: path.resolve(__dirname, 'path/to/app')
});
// HeadCollector get data from `defineHead()`
app.registerCollector("head", new HeadCollector());
// ssr
const ssr = new MultiRoutesRenderer({app});
ssr.renderToString()
.then(result => {
console.log(result);
// [{route: "/", html: "<html><head>user 1</head>...</html>"}]
})
.catch(err => console.log(err));Redux preloaded state
How Coren render __PRELOADED_STATE__
React
@collector()
export default class Product extends Component {
// Fetch data first
// then, during serverside render, put `window.__PRELOADED_STATE__=${state}` to HTML
static definePreloadedState({db}) {
return db.fetch('products').exec()
.then(data => ({products: data}));
}
render() {
return <div>
...
</div>;
}
}Collector
In ReduxCollector, we push promise we got from definePreloadedState
then, we wait all promises done at appWillRender
Last, we wrap your app with react-redux provider, and get state from store.getState(), append the state to head
class ReduxCollector {
// ...
componentDidImport(id, component) {
const promise = component.definePreloadedState(this.componentProps);
this.queries.push(promise);
}
appWillRender() {
return Promise.map(this.queries,
state => Object.assign(this.initialState, state));
}
wrapElement(appElement) {
const store = createStore(this.reducers, this.initialState);
const wrapedElements = react.createElement(Provider, {store}, appElement);
this.state = store.getState();
return wrapedElements;
}
appendToHead($head) {
$head.append(`<script>
window.__PRELOADED_STATE__ = ${JSON.stringify(this.state)}
</script>`);
}
}Serverside
const app = new App({
path: path.resolve(__dirname, 'path/to/app')
});
// ReduxCollector get initialState from `definePreloadedState()`
app.registerCollector("redux", new ReduxCollector({
// componentProps will be passed to
componentProps: {
db
},
// reducer of your app
reducers: reducer
}));
// ssr
const ssr = new MultiRoutesRenderer({app});
ssr.renderToString()
.then(result => {
console.log(result);
// [{route: "/", html: "<html><body>window.__PRELOADED_STATE__={...}</body>></html>"}]
})
.catch(err => console.log(err));Concepts
Define
Coren render html base on data gotten from Component.
so, where do Component write what they could provide for Serverside render?
Component should use @collector decorator outside, and use static method, prefixed with define. In this case, @collector could return data back to server during right lifecycle.
Lifecycle Hook
We metioned lifecycle above. How does this work?
let us take a look at collector decorator
export default function() {
return WrappedComponent => {
const uniqId = shortid.generate();
/*
trigger componentDidImport lifecycle here
notify collectors
*/
hook.componentDidImport(uniqId, WrappedComponent);
class Hoc extends React.Component {
constructor(props) {
super(props);
/*
trigger componentDidConstruct lifecycle here
pass props to collectors
*/
hook.componentDidConstruct(uniqId, WrappedComponent, props);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
return hoistStatic(Hoc, WrappedComponent);
};
}During serverside render, two lifecycle will be triggered
componentDidImport(id, component): called when component importedcomponentDidConstruct(id, component, props): called when component constructed
Why these two methods?
In React-router, only component matched with route will be rendered. So, component rendered will trigger both methods, on the other hand, component not rendered will trigger only componentDidImport. It will help you put right data in your HTML.
For Example, we should only put the head tags return from first constructed component. Components that didn't trigger componentDidConstruct should not be considered.
Collector
What is a Collector?
Collector collect data from define methods, collector can choose which lifecycle it want to call define method.
For example, we take a look at HeadCollector, HeadCollector call defineHead(props) in componentDidConstruct, it get {title, description}, then push to heads array.
when serverside renderer call appendToHead, HeadCollector push the first head it got from component to $head
class HeadCollector {
constructor() {
this.heads = [];
}
// ...
componentDidConstruct(id, component, props) {
this.heads.push(component.defineHead(props));
}
getFirstHead() {
return this.heads[0] || {};
}
// ...
appendToHead($head) {
const {title, description} = this.getFirstHead();
$head.append(`<title>${title}</title>`);
$head.append(`<meta name="description" content="${description}">`);
}
}App
App represent your react application. developer use App to register collector
// create App with path to your React App entry file
const app = new App({
path: path.resolve(__dirname, 'path/to/app')
});
// register collector
app.registerCollector("head", new HeadCollector());App controlls lifecycle of all registered collectors.
Serverside renderer will call App's lifecycle method at certain time, to get the desired result it want.
Serverside Renderer
The main purpose of Serverside Renderer is to create HTML. By calling App to controll lifecycle of collectors, make sure collectors get the result they want.
Collector Lifecycle
In MultiRoutesRenderer, every collector will go through same phases:
componentDidImport(id, component): when component importedappWillRender: do some async work here if you want to make some api call before renderrouteWillRender: when rendering multiple routes, appWillRender will be called every time the route match with your component and trigger render, so is every method belowwrapElement: you can wrap your app reactElement if you need a provider outside- (app renderToString) => ssrRenderer will call ReactDom.renderToString
componentDidConstruct(id, component, props): called when component was constructedappendToHead($cheerio('head')): append any html to headappendToBody($cheerio('body')): append any html to body
API
App
constructor({path: String})
- path: path to your React app entry file
const app = new App({
path: path.resolve(__dirname, 'path/to/app')
});registerCollector(key: String, collector: Collector)
- key: you can directly access to collector by key
app.getCollector("head")
// return headCollector- collector: the collector you want to register
app.registerCollector("head", new HeadCollector());Collector
ifEnter(component): Boolean
app will use ifEnter to determine whether call this collector or not
componentDidImport(id, component): void
called when component imported, when component imported, a unique id attached to it, so you'll know where this component appeared before or not in componentDidConstruct.
componentDidConstruct(id: String, component: ReactComponent, props: Object): void
called when component was constructed
appWillRender(): Promise
Because we react wont wait for your async code during import. So a better way to use async related task is to push your promise to an array, wait for them in appWillRender.
Take reduxCollector for example:
// /src/reduxCollector
componentDidImport(id, component) {
const promise = component.definePreloadedState(this.componentProps);
this.queries.push(promise);
}
appWillRender() {
return Promise.map(this.queries,
state => Object.assign(this.initialState, state));
}routeWillRender(): void
In MultiRoutesRenderer, you'll have multiple routes to be rendered, so you need a hook to tell your collector when a route is going to be rendered. You can do some reset variable things here.
Take HeadCollector for example, we make sure we collect fresh head from component constructed.
componentDidConstruct(id, component, props) {
this.heads.push(component.defineHead(props));
}
routeWillRender() {
// empty heads
this.heads = [];
}wrapElement(ReactElement): ReactElement
Some module require developer wrap ReactElement with provider in serverside render.
Take reduxCollector for example, we wrap ReactElement with react-redux provider.
wrapElement(appElement) {
const store = createStore(this.reducers, this.initialState);
const wrapedElements = react.createElement(Provider, {store}, appElement);
this.state = store.getState();
return wrapedElements;
}appendToHead($head: cheerio)
append any html to head
appendToBody($body: cheerio)
append any html to body
Usage
Getting Started
- npm install coren --save
- use @collector in your component
import collector from 'coren/lib/client/collectorHoc';
@collector()
export default class UserList extends Component {
// ...
render() {
...
}
}- write
definemethod.
@collector()
export default class UserList extends Component {
static defineHead() {
return {
title: "user list",
description: "user list"
};
}
static defineRoutes({Url}) {
return new Url('/users');
}
static definePreloadedState({db}) {
return db.users.find().execAsync()
.then(list => ({
users: {
list,
fetched: true,
isFetching: false,
error: false
}
}));
}
}- serverside render
serverside render with
appandmultiRoutesRenderer
const db = mongodb;
const app = new App({
path: path.resolve(__dirname, 'path/to/app')
});
// register collectors
app.registerCollector("head", new HeadCollector());
app.registerCollector("routes", new RoutesCollector({
componentProps: {
db
}
}));
app.registerCollector("redux", new ImmutableReduxCollector({
componentProps: {
db
},
reducers: reducer
}));
// ssr
const ssr = new MultiRoutesRenderer({
app,
// bundle path will be append to html body
js: ["/bundle.js"]
});
// get the array of html result
ssr.renderToString()
.then(results => {
return Promise.all(results.map(result => {
// throw HTML to anywhere you want
// cached to web server, cache server
// write to s3, cdn
}));
})
.catch(err => console.log(err));How to create own Collector
Write your own class, implement methods in Collector.
Take a look at built-in collector for reference.
https://github.com/Canner/coren/tree/master/server/collectors
Example
Here's a example repo using this module. https://github.com/Canner/coren-example