Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example of how to prefetch ssr data #5

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicons/favicon.ico">
<!--

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicons/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tag above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Expand All @@ -13,14 +14,18 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Server Side Rendering - Create React App</title>
</head>
<body>
<noscript>
Please enable javascript for this page
</noscript>
<div id="root">{{SSR}}</div>
<!--
<title>Server Side Rendering - Create React App</title>
</head>

<body>
<noscript>
Please enable javascript for this page
</noscript>
<div id="root">{{SSR}}</div>
<script>
DATA = {{WINDOW_DATA}}
</script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

Expand All @@ -30,5 +35,6 @@
To begin the development, run `npm start`.
To create a production bundle, use `npm run build`.
-->
</body>
</html>
</body>

</html>
105 changes: 79 additions & 26 deletions server/universal.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,95 @@ const path = require('path')
const fs = require('fs')

const React = require('react')
const {Provider} = require('react-redux')
const {renderToString} = require('react-dom/server')
const {StaticRouter} = require('react-router-dom')
const { Provider } = require('react-redux')
const { renderToString } = require('react-dom/server')
const { StaticRouter } = require('react-router-dom')

const {default: configureStore} = require('../src/store')
const {default: App} = require('../src/containers/App')
const { default: configureStore } = require('../src/store')
import ApiClient from '../src/ApiClient';
const { default: App } = require('../src/containers/App')
import FirstPage from '../src/containers/FirstPage'
import SecondPage from '../src/containers/SecondPage'
import NoMatch from '../src/components/NoMatch'

import { matchPath } from 'react-router-dom'

const routes = [
{
path: '/',
exact: true,
component: FirstPage,
},
{
path: '/second',
component: SecondPage,
},
{
component: NoMatch
}
]

module.exports = function universalLoader(req, res) {
const filePath = path.resolve(__dirname, '..', 'build', 'index.html')

fs.readFile(filePath, 'utf8', (err, htmlData)=>{
fs.readFile(filePath, 'utf8', (err, htmlData) => {
if (err) {
console.error('read err', err)
return res.status(404).end()
}
const context = {}
const store = configureStore()
const markup = renderToString(
<Provider store={store}>
<StaticRouter
location={req.url}
context={context}
>
<App/>
</StaticRouter>
</Provider>
)

if (context.url) {
// Somewhere a `<Redirect>` was rendered
redirect(301, context.url)
} else {
// we're good, send the response
const RenderedApp = htmlData.replace('{{SSR}}', markup)
res.send(RenderedApp)

// we'd probably want some recursion here so our routes could have
// child routes like `{ path, component, routes: [ { route, route } ] }`
// and then reduce to the entire branch of matched routes, but for
// illustrative purposes, sticking to a flat route config
const matches = routes.reduce((matches, route) => {
const match = matchPath(req.url, route.path, route)
if (match) {
matches.push({
route,
match,
promise: route.component.fetchData ?
route.component.fetchData(match) : Promise.resolve(null)
})
}
return matches
}, [])

if (matches.length === 0) {
res.status(404)
}

const promises = matches.map((match) => match.promise)

Promise.all(promises).then(data => {
// do something w/ the data so the client
// can access it then render the app
console.log('data', data[0]);
const context = {}
const client = new ApiClient();
const store = configureStore(client)
const markup = renderToString(
<Provider store={store}>
<StaticRouter
location={req.url}
context={context}
>
<App routes={routes} initialData={data[0]} />
</StaticRouter>
</Provider>
)

if (context.url) {
// Somewhere a `<Redirect>` was rendered
redirect(301, context.url)
} else {
// we're good, send the response
const RenderedApp = htmlData.replace('{{SSR}}', markup).replace('{{WINDOW_DATA}}', JSON.stringify(data[0]));
res.send(RenderedApp)
}
}, (error) => {
handleError(res, error)
})
})
}

35 changes: 35 additions & 0 deletions src/ApiClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import superagent from 'superagent';

const methods = ['get', 'post', 'put', 'patch', 'del'];

export default class ApiClient {
constructor(req) {
methods.forEach(method => {
this[method] = (path, { params, data, headers, files, fields } = {}, isExternal = false) => new Promise((resolve, reject) => {
let request;
request = superagent[method](path);

if (params) {
request.query(params);
}

if (headers) {
request.set(headers);
}

if (files) {
files.forEach(file => request.attach(file.key, file.value));
}

if (fields) {
fields.forEach(item => request.field(item.key, item.value));
}

if (data) {
request.send(data);
}
request.end((err, { body } = {}) => (err ? reject(body || err) : resolve(body)));
});
});
}
}
10 changes: 10 additions & 0 deletions src/actions/user.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { SET, RESET } from '../types/user'
const LOAD = 'LOAD';
const LOAD_SUCCESS = 'LOAD_SUCCESS';
const LOAD_FAIL = 'LOAD_FAIL';

export function tes() {
return {
types: [LOAD, LOAD_SUCCESS, LOAD_FAIL],
promise: client => client.get('https://jsonplaceholder.typicode.com/posts/3')
}
}

export function set(payload){
return {
Expand Down
20 changes: 14 additions & 6 deletions src/containers/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,25 @@ import SecondPage from './SecondPage'
import NoMatch from '../components/NoMatch'

export default class App extends Component {
render(){
render() {
const MyFirstPage = (props) => {
return (
<FirstPage
initialData={this.props.initialData}
{...props}
/>
);
}
return (
<div>
<h1>Server Side Rendering with Create React App v2</h1>
{/*<h1>Server Side Rendering with Create React App v2</h1>
<p>Hey, so I've rewritten this example with react-router v4</p>
<p>This code is on github: <a href='https://github.com/ayroblu/ssr-create-react-app-v2'>https://github.com/ayroblu/ssr-create-react-app-v2</a></p>
<p>Medium article: <a href='https://medium.com/@benlu/ssr-with-create-react-app-v2-1b8b520681d9'>https://medium.com/@benlu/ssr-with-create-react-app-v2-1b8b520681d9</a></p>
<p>Medium article: <a href='https://medium.com/@benlu/ssr-with-create-react-app-v2-1b8b520681d9'>https://medium.com/@benlu/ssr-with-create-react-app-v2-1b8b520681d9</a></p>*/}
<Switch>
<Route exact path="/" component={FirstPage}/>
<Route path="/second" component={SecondPage}/>
<Route component={NoMatch}/>
<Route exact path="/" component={MyFirstPage} />
<Route path="/second" component={SecondPage} />
<Route component={NoMatch} />
</Switch>
</div>
)
Expand Down
40 changes: 39 additions & 1 deletion src/containers/FirstPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,53 @@ import * as userActions from '../actions/user'
import { Link } from 'react-router-dom'
import './FirstPage.css'

import request from 'superagent';

class FirstPage extends Component {

// called in the server render, or in cDM
static fetchData(match) {
// going to want `match` in here for params, etc.
return new Promise((resolve, reject) => {
request.get('https://jsonplaceholder.typicode.com/posts/2').end((err, success) => {
if (err) {
reject(err);
}
resolve(success.body);
});
});
}

state = {
// if this is rendered initially we get data from the server render
data: this.props.initialData || null
}

componentDidMount() {
// if rendered initially, we already have data from the server
// but when navigated to in the client, we need to fetch
if (!this.state.data) {
this.constructor.fetchData(this.props.match).then(data => {
this.setState({ data })
})
}
this.props.userActions.tes();
}

render() {
const b64 = this.props.staticContext ? 'wait for it' : window.btoa('wait for it')
return (
<div className='bold'>
<h2>First Page</h2>
<p>{`Email: ${this.props.user.email}`}</p>
<p>{`b64: ${b64}`}</p>
<Link to={'/second'}>Second</Link>
<Link to={'/second'}>Second</Link><br/>
<p><strong>The text below is a prefetched SSR data:</strong></p>
{this.state.data &&
<h2>
{this.state.data.id} - {this.state.data.title}
</h2>
}
</div>
)
}
Expand Down
26 changes: 26 additions & 0 deletions src/createMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export default function clientMiddleware(client) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}

const { promise, types, ...rest } = action; // eslint-disable-line no-redeclare
if (!promise) {
return next(action);
}

const [REQUEST, SUCCESS, FAILURE] = types;
next({ ...rest, type: REQUEST });

const actionPromise = promise(client, dispatch);
actionPromise.then(
result => next({ ...rest, result, type: SUCCESS }),
error => next({ ...rest, error, type: FAILURE })
).catch((error) => {
console.error('MIDDLEWARE ERROR:', error);
next({ ...rest, error, type: FAILURE });
});

return actionPromise;
};
}
29 changes: 25 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,39 @@ import { BrowserRouter } from 'react-router-dom'
import configureStore from './store'
import './index.css'
import App from './containers/App'
import ApiClient from './ApiClient'

import FirstPage from './containers/FirstPage'
import SecondPage from './containers/SecondPage'
import NoMatch from './components/NoMatch'

const routes = [
{
path: '/',
exact: true,
component: FirstPage
},
{
path: '/second',
component: SecondPage,
},
{
component: NoMatch
}
]

// Let the reducers handle initial state
const initialState = {}
const store = configureStore(initialState)
const initialState = {};
const client = new ApiClient();
const store = configureStore(client, initialState)

ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
<App routes={routes} initialData={window.DATA} />
</BrowserRouter>
</Provider>
, document.getElementById('root')
, document.getElementById('root')
)


Loading