diff --git a/public/index.html b/public/index.html index 74da614..9668526 100644 --- a/public/index.html +++ b/public/index.html @@ -1,10 +1,11 @@ - - - - - - Server Side Rendering - Create React App - - - -
{{SSR}}
- - - + + + \ No newline at end of file diff --git a/server/universal.js b/server/universal.js index 5de8773..1d85c96 100644 --- a/server/universal.js +++ b/server/universal.js @@ -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( - - - - - - ) - - if (context.url) { - // Somewhere a `` 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( + + + + + + ) + + if (context.url) { + // Somewhere a `` 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) + }) }) } diff --git a/src/ApiClient.js b/src/ApiClient.js new file mode 100644 index 0000000..e269c02 --- /dev/null +++ b/src/ApiClient.js @@ -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))); + }); + }); + } +} diff --git a/src/actions/user.js b/src/actions/user.js index d42e16b..f12c172 100644 --- a/src/actions/user.js +++ b/src/actions/user.js @@ -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 { diff --git a/src/containers/App.js b/src/containers/App.js index 005ba70..5cb69be 100644 --- a/src/containers/App.js +++ b/src/containers/App.js @@ -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 ( + + ); + } return (
-

Server Side Rendering with Create React App v2

+ {/*

Server Side Rendering with Create React App v2

Hey, so I've rewritten this example with react-router v4

This code is on github: https://github.com/ayroblu/ssr-create-react-app-v2

-

Medium article: https://medium.com/@benlu/ssr-with-create-react-app-v2-1b8b520681d9

+

Medium article: https://medium.com/@benlu/ssr-with-create-react-app-v2-1b8b520681d9

*/} - - - + + +
) diff --git a/src/containers/FirstPage.js b/src/containers/FirstPage.js index 04fc4ec..fb39d34 100644 --- a/src/containers/FirstPage.js +++ b/src/containers/FirstPage.js @@ -6,7 +6,39 @@ 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 ( @@ -14,7 +46,13 @@ class FirstPage extends Component {

First Page

{`Email: ${this.props.user.email}`}

{`b64: ${b64}`}

- Second + Second
+

The text below is a prefetched SSR data:

+ {this.state.data && +

+ {this.state.data.id} - {this.state.data.title} +

+ } ) } diff --git a/src/createMiddleware.js b/src/createMiddleware.js new file mode 100644 index 0000000..9f4f1e9 --- /dev/null +++ b/src/createMiddleware.js @@ -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; + }; +} diff --git a/src/index.js b/src/index.js index fb6bed7..ff4cb7e 100644 --- a/src/index.js +++ b/src/index.js @@ -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( - + -, document.getElementById('root') + , document.getElementById('root') ) diff --git a/src/reducers/user.js b/src/reducers/user.js index c5f6398..a91db51 100644 --- a/src/reducers/user.js +++ b/src/reducers/user.js @@ -1,15 +1,42 @@ import { SET, RESET } from '../types/user' +const LOAD = 'LOAD'; +const LOAD_SUCCESS = 'LOAD_SUCCESS'; +const LOAD_FAIL = 'LOAD_FAIL'; const initialState = { - email: 'user@example.com' + loaded: false, + loading: false, + email: 'user@example.com', + userId: 1, + id: 1, + title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", + body: "quia et suscipit suscipit recusandae consequuntur expedita et cum reprehenderit molestiae ut ut quas totam nostrum rerum est autem sunt rem eveniet architecto" } -export default function reducer(state=initialState, action) { +export default function reducer(state = initialState, action) { switch (action.type) { + case LOAD: + return { + ...state, + loading: true, + } + case LOAD_SUCCESS: + return { + ...state, + loaded: true, + loading: false, + ...action.result + } + case LOAD_FAIL: + return { + loaded: true, + loading: false, + error: action.error + } case SET: - return {...state, ...action.payload} + return { ...state, ...action.payload } case RESET: - return {...initialState} + return { ...initialState } default: return state } diff --git a/src/store.js b/src/store.js index 9891af9..02e55b6 100644 --- a/src/store.js +++ b/src/store.js @@ -1,14 +1,16 @@ import { createStore, applyMiddleware, compose } from 'redux' import reducers from './reducers' +import createMiddleware from './createMiddleware'; //import createLogger from 'redux-logger' //import createSagaMiddleware from 'redux-saga' //const logger = createLogger() //const sagaMiddleware = createSagaMiddleware() -export default function configureStore(initialState = {}) { +export default function configureStore(client, initialState = {}) { // Create the store with two middlewares const middlewares = [ + createMiddleware(client) // sagaMiddleware //, logger ]