Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@stoikerty
Last active March 10, 2017 19:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stoikerty/40a668e8fd4e2919034fd1eed2252bcb to your computer and use it in GitHub Desktop.
Save stoikerty/40a668e8fd4e2919034fd1eed2252bcb to your computer and use it in GitHub Desktop.
Setting up the `dynamic-pages`-package with `dev-toolkit` [ https://github.com/stoikerty/dev-toolkit ]
// ----- src/server/dynamicRender.js -----
// IMPORTANT:
// `dynamic-pages` will look for this file to render a page for each route.
// This file acts in a very similar way to the `router.jsx`
import { match, createMemoryHistory } from 'react-router';
// separate utility mentioned below
import { generateReactHTML } from 'src/server/utils';
// if you use redux, grab what you need from the client
import store from '../client/redux/store';
import routes from '../client/routes';
// create empty fetch method because it's not used while generating a build
global.fetch = () => new Promise(() => {});
// PS: you will have to wrap certain client-only callbacks in `if (typeof window !== 'undefined')`
// alternatively, you can use the exposed `isClient` variable from `settings.js`
export default (location) => {
const browserHistory = createMemoryHistory();
let reactHtml = null;
const initialReduxState = store.getState();
match({ routes, location }, (error, redirectLocation, renderProps) => {
reactHtml = generateReactHTML({ store, routes, browserHistory, renderProps });
});
return {
reactHtml,
// additionalData allows you to pass arbitrary data into the template.
// In the template, each comment written like this:
// <!-- [[[initialReduxState]]] -->
// is replaced with the respective key defined in `additionalData`
additionalData: {
// set up you initial redux state like this
initialReduxState: `<script>window.__PRELOADED_STATE__ = ${JSON.stringify(initialReduxState)}</script>`,
myCustomData: 'I am going to be in each dynamic page if I am defined correctly in the template',
// `dynamicComponents` is a reserved key and should not be used
},
};
};
// ----- src/server/utils/generateReactHTML.js -----
// You can try using `renderToString` though I recommend `renderToStaticMarkup` as it's much easier to get everything working.
// Server-side rendering is a PITA, but once you understand the fallbacks it will become easier.
import React from 'react';
import ReactDOM from 'react-dom/server';
import { Router, RouterContext } from 'react-router';
import { Provider } from 'react-redux';
export default ({ store, routes, browserHistory, renderProps }) => (
browserHistory
// Dynamic Page Rendering with history and <Router> wrapper
? ReactDOM.renderToStaticMarkup((
<Provider store={store}>
<Router
routes={routes}
history={browserHistory}
render={() => <RouterContext {...renderProps} />}
/>
</Provider>
))
// Pure Server-rendering doesn't need history, only <RouterContext>
: ReactDOM.renderToStaticMarkup((
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>
))
);
// ----- src/server/views/layout.hbs -----
// All dynamic components must be defined below with at least `<!-- [[[dynamicComponents]]] -->`
...
</head>
<body>
<!-- The Markup Container -->
<div data-jshook="app-body">{{{htmlWebpackPlugin.options.reactHtml}}}{{{server.reactHtml}}}</div>
<!-- [[[myCustomData]]] -->
{{{server.myOtherCustomData}}}
<!-- [[[initialReduxState]]] -->
{{{server.initialReduxState}}}
{{#if htmlWebpackPlugin.options.creatingBuild}}
{{#each htmlWebpackPlugin.files.chunks}}
<script src="{{this.entry}}"></script>
{{/each}}
{{else}}
<script src="/vendor.js"></script>
<script src="/app.js" async defer></script>
{{/if}}
<!-- [[[dynamicComponents]]] -->
{{{server.dynamicComponents}}}
</body>
</html>
// ----- src/server/router.jsx -----
// The router is indirectly used while generating a build
// and always used during development with --watch
import { match } from 'react-router';
import DynamicPages from 'dynamic-pages';
// Use this if you want to develop with or without server-rendering during development
const usesServerRendering = true;
// same util as in dynamicRender
import { generateReactHTML } from 'src/server/utils';
// set up redux if you have it
import store from '../client/redux/store';
import routes from '../client/routes';
// React Router Boilerplate
// Note:
// Adapted from server-rendering example: https://github.com/rackt/react-router/blob/latest/docs/guides/advanced/ServerRendering.md
export default (req, res) => {
const initialReduxState = store.getState();
const location = req.url;
match({ routes, location }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message);
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
const reactHtml = usesServerRendering ?
generateReactHTML({ store, routes, renderProps }) : '';
// Render `layout`-template using Handlebars
res.status(200).render('layout', {
// and with custom `server` variable that is separate from `htmlWebpackPlugin`
// Each variable defined in the `layout.hbs like this:
// {{{server.initialReduxState}}}
// will be replaced with the respective key defined below
server: {
reactHtml,
// set up initialReduxState if you need it
initialReduxState: `<script>window.__PRELOADED_STATE__ = ${JSON.stringify(initialReduxState)}</script>`,
// IMPORTANT:
// Where each component gets inserted with `dynamic-pages`. Makes sure dynamic components are loaded before the app starts.
// Without this you will keep getting "React attempted to reuse markup"-warnings.
dynamicComponents: DynamicPages.generateScripts({ renderPath: location }),
myOtherCustomData: 'I am going to be rendered by the server each time during development if I am defined correctly in the template',
// I have some envs set up, feel free to remove if you don't use it
env: JSON.stringify(process.env),
},
});
} else {
res.status(404).send('Not found');
}
});
};
// ----- src/client/routes.jsx -----
...
import DynamicPages from 'dynamic-pages';
...
// This is optional, but I find it really useful. It's very "meteor-like"
import { isClient } from 'src/settings';
// Shell is a regular component that acts as a surrounding container for all routes
import Shell from './views/Shell';
// import all dynamic components
// IMPORTANT!
// You need to suffix either `.dynamic` or `.route` to tell `dev-toolkit` that the component should be loaded asynchronously.
// Each dynamic component MUST have a unique filename where the `displayName` of the component uses the same name that the
// component uses as its filename (but without the .jsx extension). See examples below.
// To use `dynamic-pages` without `dev-toolkit` you'll need to implement `bundle`-loader in a similar way to this:
// https://github.com/stoikerty/dev-toolkit/blob/master/packages/dev-toolkit/src/webpack/config/loaders.js#L34
// Examples:
// Filename - Header.dynamic.jsx ==> displayName: 'Header.dynamic'
// Filename - Checkout.route.jsx ==> displayName: 'Checkout.route'
import Header from './shared-components/Header.dynamic';
import Home from './views/pages/Home.route';
import Checkout from './views/pages/Checkout.route';
import Finish from './views/pages/Checkout/Finish.route';
...
// Initialise `dynamic-pages` with a boolean to tell it whether it's being used on the client/server
DynamicPages.init({ isClient });
// This allows you to define components you might want to prefetch once the current page is loaded.
DynamicPages.addToPrefetch({ components: [Home, Header, Checkout, Finish] });
// Place the following line in `app.jsx` in your `domready` callback to prefetch all subsequent pages
// DynamicPages.runPrefetch({ logActivity: isDev });
// IMPORTANT: each dynamic page mus be defined with `...DynamicPages.defineRoute` as seen below.
export default (
<Route path="/" component={Shell}>
<IndexRoute
{...DynamicPages.defineRoute({
renderPath: '/',
components: {
content: Home,
},
})}
/>
<Route path="checkout">
<IndexRoute
{...DynamicPages.defineRoute({
renderPath: '/checkout',
components: {
header: Header,
content: Checkout,
},
})}
/>
</Route>
<Route path="checkout/:customerRef">
<IndexRoute
{...DynamicPages.defineRoute({
renderPath: '/checkout/:customerRef',
components: {
header: Header,
content: Checkout,
},
})}
/>
<Route
path="finish"
{...DynamicPages.defineRoute({
renderPath: '/checkout/finish',
components: {
header: Header,
content: Finish,
},
})}
/>
</Route>
</Route>
);
// ----- src/settings.js -----
// Detect whether the app is being rendered on the client or on the server
const creatingBuild = typeof buildSettings !== typeof undefined;
const env = creatingBuild ? buildSettings.env : process.env;
// export useful variables
export const isDev = env.NODE_ENV === 'development';
export const isServer = !creatingBuild && env.NODE_ENV !== 'test';
export const isClient = !isServer;
// ----- src/client/views/Shell.jsx -----
// Dynamic components & pages are handled differently. `react-router` doesn't use the `children` object for rendered content.
// Instead it uses the keys specified in each route defined by dynamic-pages in `routes.jsx`, such as `header` and `content`.
import React, { Component, PropTypes } from 'react';
import s from './Shell/_style.scss';
export default class Shell extends Component {
static displayName = 'Shell'
static propTypes = {
children: PropTypes.element,
header: PropTypes.node,
content: PropTypes.node,
// you can figure out what page is being rendered with the router location
location: PropTypes.object.isRequired,
}
constructor(props) {
super(props);
this.state = {};
}
render() {
const { content, children } = this.props;
return (
<div className={s.Shell}>
{/* Navigation components for a specific page */}
{header || null}
{/* You can also have components here that are always displayed, like permanent navigation */}
<MyPermanentComponent />
{/* Display rendered page-content */}
{content || children}
</div>
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment