Last active
March 10, 2017 19:39
-
-
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 ]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ----- 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 | |
}, | |
}; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ----- 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> | |
)) | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ----- 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'); | |
} | |
}); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ----- 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> | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ----- 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ----- 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