Skip to content

Instantly share code, notes, and snippets.

@XeeD
Last active September 1, 2016 15:54
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save XeeD/23824f4626bd29673a44 to your computer and use it in GitHub Desktop.
Save XeeD/23824f4626bd29673a44 to your computer and use it in GitHub Desktop.
Este.js and react-transmit

Data fetching for Este with react-transmit

1) Without server rendering

Read the DOCS.

This is easy and straightforward. All you need is to add react-transmit into dependencies (npm install --save react-transmit). Transmit works as a Higher-order component so instead of

export default class Dashboard extends PureComponent {
  // ...
}

you do this

import Transmit from 'react-transmit';

export class Dashboard extends PureComponent {
 // ...
}

export default Transmit.createContainer({
  queries: {
    clientData() {
      // return a Promise here
    }
  }
})

The component then has the result of the Promise inside this.props.clientData. Easy.

2) With server rendering

State manipulation and race condition

There is a possible race condition in Este when you use Transmit.renderToString - este/este#202 Transmit calls React.renderToString twice. First it needs to render the app to gather all queries that need to be executed. Then it executes the queries and renders the app again, this time passing the results of the queries as props to the components.

This create a race condition because the initialState may change during resolution of the promises (IO). We need to pass a function to Transmit.renderToString which resets the initial state to the same value before calling React.renderToString again.

RouteHandler and passing of queries from first-level route

The query is probably not on the main component (App) but on the first-level route instead (think of a Dashboard component which needs to fetch client data). We need to pass the query from the Handler to the top component.

// The main component needs to pass the results of the queries as props
// with the same name to the RouteHandler component
class App extends React.Component {
static displayName = 'App'
// normal Este goes here...
render() {
const routeName = this.context.router.getCurrentPath()
const {childQueries} = this.props
return (
<DocumentTitle title='Secret project :-)'>
<div className='page-wrapper' key={routeName}>
<RouteHandler {...this.state} {...childQueries} />
</div>
</DocumentTitle>
)
}
}
// finally
export default Transmit.createContainer(App, {
queries: {
childQueries(props) {
const {matchedRoutes} = props
const handler = matchedRoutes[1] && matchedRoutes[1].handler
if (handler && handler.getAllQueries)
return handler.getAllQueries()
else
return Promise.resolve({})
}
}
})
import DocumentTitle from 'react-document-title'
import Html from './html'
import MobileDetect from 'mobile-detect'
import Promise from 'bluebird'
import React from 'react'
import Router from 'react-router'
import Transmit from 'react-transmit'
import config from './config'
import initialState from './initialstate'
import renderToString from './react-transmit/render_to_string'
import routes from '../client/routes'
import {state} from '../client/state'
import {storeCookies} from '../client/request_store'
export default function render(req, res, locale) {
const device = new MobileDetect(req.headers['user-agent']).phone() ? 'mobile' : 'desktop'
const path = req.path
return renderPage(req, res, initialState, path, device)
}
// This function sets up the state before first and second round of React.renderToString
// inside of Transmit.renderToString
function beforeRender(req, appState) {
return function beforeRender() {
state.load(appState)
storeCookies(req)
}
}
// TODO: Refactor.
function renderPage(req, res, appState, path, device) {
return new Promise((resolve, reject) => {
const router = Router.create({
routes,
location: path,
onError: reject,
onAbort: (abortReason) => {
if (abortReason.constructor.name === 'Redirect') {
const {to, params, query} = abortReason
const path = router.makePath(to, params, query)
res.redirect(path)
resolve()
return
}
reject(abortReason)
}
})
router.run((Handler, routerState) => {
const notFound = routerState.routes.some(route => route.name === 'not-found')
const status = notFound ? 404 : 200
// getPageHtml now returns a Promise
getPageHtml(Handler, appState, device, routerState.routes, beforeRender)
.then(html => {
res.status(status).send(html)
resolve()
}).catch(error => {
console.error('getPageHTML error', error)
reject(error)
})
})
})
}
function getPageHtml(Handler, appState, device, matchedRoutes, beforeRender) {
// Important! We need to send the matchedRoute as queryParam to the main component
return renderToString(Handler, beforeRender, {queryParams: {matchedRoutes}}).then(result => {
const {reactString, reactData} = result
const appHtml = `<div id="single-page-app"><div id="react-root">${reactString}</div></div>`
const appScriptSrc = config.env.isProduction
? '/app.js?v=' + config.version
: '//localhost:8888/build/public/app.js'
let scriptHtml = `
<script>
(function() {
window._appState = ${JSON.stringify(appState)};
var app = document.createElement('script'); app.type = 'text/javascript'; app.async = true;
var src = '${appScriptSrc}';
// IE<11 and Safari need Intl polyfill.
if (!window.Intl) src = src.replace('.js', 'intl.js');
app.src = src;
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(app, s);
})()
</script>`
if (config.env.isProduction && config.googleAnalyticsId !== 'UA-XXXXXXX-X')
scriptHtml += `
<script>
(function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
e=o.createElement(i);r=o.getElementsByTagName(i)[0];
e.src='//www.google-analytics.com/analytics.js';
r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
ga('create','${config.googleAnalyticsId}');ga('send','pageview');
</script>`
const title = DocumentTitle.rewind()
const outputHtml = '<!DOCTYPE html>' + React.renderToStaticMarkup(
<Html
bodyHtml={appHtml + scriptHtml}
isProduction={config.env.isProduction}
title={title}
version={config.version}
device={device}
/>
)
// This adds window.__reactTransmitPacket with results of the queries
// so that client does not need to perform the same queries again
return Transmit.injectIntoMarkup(outputHtml, reactData)
})
}
import Promise from 'bluebird'
import React from 'react'
const assign = React.__spread
export default function renderToString(Component, beforeRender, props = {}) {
return new Promise(function(resolve, reject) {
const onQuery = promise => {
promise.then(queryResults => {
const componentProps = assign({}, props, queryResults)
// Set up the world
beforeRender()
const reactString = React.renderToString(
React.createElement(Component, componentProps)
)
resolve({
reactString: reactString,
reactData: queryResults
})
}).catch(error => reject(error))
}
const myProps = assign({}, props, {onQuery: onQuery})
// ... and reset it
beforeRender()
React.renderToString(React.createElement(Component, myProps))
})
}
@Webkadabra
Copy link

Man, Thank you for sharing! I was just working on adding transmit to my Este+Loopback app, this Gist is magic!

@Webkadabra
Copy link

I found that adding @Flux(store) on a root "App" component will conflict somehow with react-transmit and queried data is not available to be rendered on server. Removing this decorator makes it all work.

Do you have any ideas? I'm kind of new to Node, so I will work on this to fix it, but any advice is greatly appreciated!

@Webkadabra
Copy link

I was able to fix it by modifying lib/flux/decorator, making it to pass this.props as well as state, like this:
render() {
      return <BaseComponent {...this.state} {...this.props} />;
    }

@XeeD
Copy link
Author

XeeD commented Sep 4, 2015

@Webkadabra I wrote this manual before the new Este Flux was introduced so I didn't try it with it yet. Thanks for sharing the fix!

@przeor
Copy link

przeor commented Sep 1, 2016

Hi I see that you use React, so I am sure that you will find interesting the https://reactjs.co - this is the free online convention and tutorial book for React.JS Developers. React is not only the View (in MVC) anymore. ReactJS For Dummies: Why & How to Learn React Redux, the Right Way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment