Skip to content

Commit 9c16c7d

Browse files
Beginning server-side rendering support
1 parent 5811c98 commit 9c16c7d

File tree

15 files changed

+208
-47
lines changed

15 files changed

+208
-47
lines changed

samples/react/MusicStore/ReactApp/TypedRedux.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export function isActionType<T extends Action>(action: Action, actionClass: Acti
2020
return action.type == actionClass.prototype.type;
2121
}
2222

23+
// Middleware for transforming Typed Actions into plain actions
24+
export const typedToPlain = (store: any) => (next: any) => (action: any) => {
25+
next(Object.assign({}, action));
26+
};
27+
2328
export abstract class Action {
2429
type: string;
2530
constructor() {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from 'react';
2+
import { Provider } from 'react-redux';
3+
import { renderToString } from 'react-dom/server';
4+
import { match, RouterContext } from 'react-router';
5+
React;
6+
7+
import { routes } from './routes';
8+
import configureStore from './configureStore';
9+
import { ApplicationState } from './store';
10+
11+
export default function (params: any, callback: (err: any, result: { html: string, store: Redux.Store }) => void) {
12+
match({ routes, location: params.location }, (error, redirectLocation, renderProps: any) => {
13+
try {
14+
if (error) {
15+
throw error;
16+
}
17+
18+
const store = configureStore(params.history, params.state);
19+
const html = renderToString(
20+
<Provider store={ store }>
21+
<RouterContext {...renderProps} />
22+
</Provider>
23+
);
24+
25+
callback(null, { html, store });
26+
} catch (error) {
27+
callback(error, null);
28+
}
29+
});
30+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import * as React from 'react';
22
import * as ReactDOM from 'react-dom';
3-
import { browserHistory } from 'react-router';
3+
import { browserHistory, Router } from 'react-router';
44
import { Provider } from 'react-redux';
55
React; // Need this reference otherwise TypeScript doesn't think we're using it and ignores the import
66

77
import './styles/styles.css';
88
import 'bootstrap/dist/css/bootstrap.css';
99
import configureStore from './configureStore';
10-
import { App } from './components/App';
10+
import { routes } from './routes';
1111

1212
const store = configureStore(browserHistory);
1313

1414
ReactDOM.render(
1515
<Provider store={ store }>
16-
<App history={ browserHistory } />
16+
<Router history={ browserHistory } children={ routes } />
1717
</Provider>,
1818
document.getElementById('react-app')
1919
);

samples/react/MusicStore/ReactApp/components/App.tsx

Lines changed: 0 additions & 37 deletions
This file was deleted.

samples/react/MusicStore/ReactApp/configureStore.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
22
import * as thunkModule from 'redux-thunk';
33
import { syncHistory, routeReducer } from 'react-router-redux';
44
import * as Store from './store';
5+
import { typedToPlain } from './TypedRedux';
56

67
export default function configureStore(history: HistoryModule.History, initialState?: Store.ApplicationState) {
78
// Build middleware
89
const thunk = (thunkModule as any).default; // Workaround for TypeScript not importing thunk module as expected
910
const reduxRouterMiddleware = syncHistory(history);
10-
const middlewares = [thunk, reduxRouterMiddleware];
11-
const devToolsExtension = (window as any).devToolsExtension; // If devTools is installed, connect to it
11+
const middlewares = [thunk, reduxRouterMiddleware, typedToPlain];
12+
const devToolsExtension = null;//(window as any).devToolsExtension; // If devTools is installed, connect to it
1213

1314
const finalCreateStore = compose(
1415
applyMiddleware(...middlewares),
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const domain = require('domain') as any;
2+
const domainContext = require('domain-context') as any;
3+
const domainTasksStateKey = '__DOMAIN_TASKS';
4+
5+
export function addTask(task: PromiseLike<any>) {
6+
if (task && domain.active) {
7+
const state = domainContext.get(domainTasksStateKey) as DomainTasksState;
8+
if (state) {
9+
state.numRemainingTasks++;
10+
task.then(() => {
11+
// The application may have other listeners chained to this promise *after*
12+
// this listener. Since we don't want the combined task to complete until
13+
// all the handlers for child tasks have finished, delay the following by
14+
// one tick.
15+
setTimeout(() => {
16+
state.numRemainingTasks--;
17+
if (state.numRemainingTasks === 0) {
18+
state.triggerResolved();
19+
}
20+
}, 0);
21+
}, state.triggerRejected);
22+
}
23+
}
24+
}
25+
26+
export function run(codeToRun: () => void): Promise<void> {
27+
return new Promise((resolve, reject) => {
28+
domainContext.runInNewDomain(() => {
29+
const state: DomainTasksState = {
30+
numRemainingTasks: 0,
31+
triggerResolved: resolve,
32+
triggerRejected: reject
33+
};
34+
domainContext.set(domainTasksStateKey, state);
35+
codeToRun();
36+
37+
// If no tasks were registered synchronously, then we're done already
38+
if (state.numRemainingTasks === 0) {
39+
resolve();
40+
}
41+
});
42+
}) as any as Promise<void>;
43+
}
44+
45+
interface DomainTasksState {
46+
numRemainingTasks: number;
47+
triggerResolved: () => void;
48+
triggerRejected: () => void;
49+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
declare module 'isomorphic-fetch' {
2-
export default function fetch(url: string): Promise<any>;
2+
export default function fetch(url: string, opts: any): Promise<any>;
33
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
var createMemoryHistory = require('history/lib/createMemoryHistory');
2+
var url = require('url');
3+
var babelCore = require('babel-core');
4+
var babelConfig = {
5+
presets: ["es2015", "react"]
6+
};
7+
8+
var origJsLoader = require.extensions['.js'];
9+
require.extensions['.js'] = loadViaBabel;
10+
require.extensions['.jsx'] = loadViaBabel;
11+
12+
function loadViaBabel(module, filename) {
13+
// Assume that all the app's own code is ES2015+ (optionally with JSX), but that none of the node_modules are.
14+
// The distinction is important because ES2015+ forces strict mode, and it may break ES3/5 if you try to run it in strict
15+
// mode when the developer didn't expect that (e.g., current versions of underscore.js can't be loaded in strict mode).
16+
var useBabel = filename.indexOf('node_modules') < 0;
17+
if (useBabel) {
18+
var transformedFile = babelCore.transformFileSync(filename, babelConfig);
19+
return module._compile(transformedFile.code, filename);
20+
} else {
21+
return origJsLoader.apply(this, arguments);
22+
}
23+
}
24+
25+
var domainTasks = require('./domain-tasks.js');
26+
var bootServer = require('./boot-server.jsx').default;
27+
28+
function render(requestUrl, callback) {
29+
var store;
30+
var params = {
31+
location: url.parse(requestUrl),
32+
history: createMemoryHistory(requestUrl),
33+
state: undefined
34+
};
35+
36+
// Open a new domain that can track all the async tasks commenced during first render
37+
domainTasks.run(function() {
38+
// Since route matching is asynchronous, add the rendering itself to the list of tasks we're awaiting
39+
domainTasks.addTask(new Promise(function (resolve, reject) {
40+
// Now actually perform the first render that will match a route and commence associated tasks
41+
bootServer(params, function(error, result) {
42+
if (error) {
43+
reject(error);
44+
} else {
45+
// The initial 'loading' state HTML is irrelevant - we only want to capture the state
46+
// so we can use it to perform a real render once all data is loaded
47+
store = result.store;
48+
resolve();
49+
}
50+
});
51+
}));
52+
}).then(function() {
53+
// By now, all the data should be loaded, so we can render for real based on the state now
54+
params.state = store.getState();
55+
bootServer(params, function(error, result) {
56+
if (error) {
57+
callback(error, null);
58+
} else {
59+
var html = result.html + `<script>window.__INITIAL_STATE = ${ JSON.stringify(store.getState()) }</script>`;
60+
callback(null, html)
61+
}
62+
});
63+
}).catch(function(error) {
64+
callback(error, null);
65+
});
66+
}
67+
68+
render('/', (err, html) => {
69+
if (err) {
70+
throw err;
71+
}
72+
73+
console.log(html);
74+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from 'react';
2+
import { Router, Route, HistoryBase } from 'react-router';
3+
import NavMenu from './components/NavMenu';
4+
import Home from './components/public/Home';
5+
import Genres from './components/public/Genres';
6+
import GenreDetails from './components/public/GenreDetails';
7+
import AlbumDetails from './components/public/AlbumDetails';
8+
9+
class Layout extends React.Component<{ body: React.ReactElement<any> }, void> {
10+
public render() {
11+
return <div>
12+
<NavMenu />
13+
<div className="container">
14+
{ this.props.body }
15+
</div>
16+
</div>;
17+
}
18+
}
19+
20+
export const routes = <Route component={ Layout }>
21+
<Route path="/" components={{ body: Home }} />
22+
<Route path="/genres" components={{ body: Genres }} />
23+
<Route path="/genre/:genreId" components={{ body: GenreDetails }} />
24+
<Route path="/album/:albumId" components={{ body: AlbumDetails }} />
25+
</Route>;

samples/react/MusicStore/ReactApp/store/AlbumDetails.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import fetch from 'isomorphic-fetch';
1+
import { fetch } from '../tracked-fetch';
22
import { typeName, isActionType, Action, Reducer } from '../TypedRedux';
33
import { ActionCreator } from './';
44
import { Genre } from './GenreList';

0 commit comments

Comments
 (0)