Created
July 31, 2018 17:19
-
-
Save natterstefan/ff1314d5258727c4f52915bc31c80b6a to your computer and use it in GitHub Desktop.
React - Example Component connected to react-router server w/ prefetching data
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
/* eslint-disable no-underscore-dangle */ | |
import React from 'react' | |
import PropTypes from 'prop-types' | |
import get from 'lodash.get' | |
import BemHelper from 'react-bem-helper' | |
import { apiClient } from '../../../common/api/client' | |
// Styling | |
const classes = new BemHelper('list') | |
/** | |
* Example Component, to illustrate server-side prefetching and hydration of data | |
* | |
* inspired by (Credits): | |
* - https://alligator.io/react/react-router-ssr/ | |
*/ | |
export class List extends React.Component { | |
constructor(props) { | |
super(props) | |
if (props.staticContext && props.staticContext.data) { | |
this.state = { | |
data: props.staticContext.data, | |
} | |
} else { | |
this.state = { | |
data: [], | |
} | |
} | |
} | |
componentDidMount() { | |
setTimeout(() => { | |
if (window && get(window, '__PRELOADED_DATA__.posts')) { | |
this.setState({ | |
data: get(window, '__PRELOADED_DATA__.posts'), | |
}) | |
delete window.__PRELOADED_DATA__.posts // CAUTION: other components cannot use it anymore | |
} else { | |
apiClient('posts').then(data => { | |
this.setState({ | |
data, | |
}) | |
}) | |
} | |
}, 0) | |
} | |
render() { | |
const { data } = this.state | |
return ( | |
<ul {...classes()}> | |
{data.length > 0 && data.map(post => <li key={post.id}>{post.title}</li>)} | |
</ul> | |
) | |
} | |
} | |
List.propTypes = { | |
staticContext: PropTypes.object, // eslint-disable-line react/forbid-prop-types | |
} | |
List.defaultProps = { | |
staticContext: {}, | |
} |
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
import React from 'react' | |
import { renderToString } from 'react-dom/server' | |
import { Provider } from 'react-redux' | |
import { matchRoutes } from 'react-router-config' | |
import merge from 'lodash.merge' | |
import { Routes, ServerRouter } from '../common/routes' | |
import { createReduxStore } from '../common/createStore' | |
import { renderFullPage } from './render-full-page' | |
import { changeCounter } from '../common/actions/app' | |
import { receivePosts } from '../common/actions/posts' | |
require('dotenv').config() | |
const ts = new Date().getTime() // ts at start time | |
export const handleRender = async (req, res) => { | |
// cachebuster and redux store | |
const currentVersion = process.env.VERSION || ts | |
// Prefetch data based on the current react-router route | |
// - https://reacttraining.com/react-router/web/guides/server-rendering/data-loading | |
// - https://alligator.io/react/react-router-ssr/ | |
const matchingRoutes = matchRoutes(Routes, req.path) | |
const promises = [] | |
matchingRoutes.forEach(route => { | |
if (route.route.loadData) { | |
promises.push(route.route.loadData()) | |
} | |
}) | |
// TODO: create promise-all-never-fails utility (because of one request fails, | |
// all others would fail/be cancelled as well) | |
let preloadedData = {} | |
const result = await Promise.all(promises) | |
result.forEach(data => { | |
preloadedData = merge({}, preloadedData, data) | |
}) | |
// this context is then later accessible in the route component (eg. <App />) | |
const context = { ...preloadedData } | |
const { store } = createReduxStore(req.url) | |
// demonstration of how the store could be manipulated/prepared already on the server | |
// eg. based on data which is stored in the cookie | |
store.dispatch(changeCounter(1)) | |
store.dispatch(receivePosts(preloadedData)) | |
// Grab the current state from our Redux store to render it in the html | |
// https://redux.js.org/recipes/server-rendering#inject-initial-component-html-and-state | |
const preloadedState = store.getState() | |
// NOTE: | |
// one could dispatch an action here now (eg. for instance prepare a state, based | |
// on the cookies of the user or something else), before renderToString. | |
// Example: https://github.com/cereallarceny/cra-ssr/blob/master/server/loader.js#L59-L65 | |
const html = renderToString( | |
<Provider store={store}> | |
<ServerRouter req={req} context={context} /> | |
</Provider>, | |
) | |
// redirect based on the status or when context.url is set (eg. <Redirect /> | |
// component is used) | |
// Docs: | |
// - https://reacttraining.com/react-router/web/guides/server-rendering/adding-app-specific-context-information | |
// - https://alligator.io/react/react-router-ssr/ | |
if (context.status === 404) { | |
res.status(404) | |
} | |
if (context.url) { | |
return res.redirect(301, context.url) | |
} | |
// Send the rendered page back to the client | |
return renderFullPage({ currentVersion, html, preloadedState, preloadedData }) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment