Routing-aware React server-side rendering on PHP with Silex and react-router

Not everybody can use Node.js on the server side to be able to craft truly universal JavaScript applications. Still, one might want to set up server-side rendering to enhance React's performances, and of course allow JS-uncapable clients to use their app seamlessly (at least regarding navigation).

In , I quickly demonstrated how to render React code with PHP's v8js extension. We will now see how to handle the routing with Silex and react-router.

Note: this article's snippets are written using ES6.

First, let's define our routes:

import Root from './components/root';
import HomePage from './components/homePage';
import OtherPage from './components/otherPage';

export default {
path: '/',
component: Root,
indexRoute: {
component: HomePage
},
childRoutes: [
{
path: 'other',
component: OtherPage
}
]
};

Here are the components:

import React from 'react';

export default class Root extends React.Component
{
render()
{
return (
<div>{this.props.children}</div>
);
}
}

import React from 'react';

export default class HomePage extends React.Component
{
render()
{
return (
<div>Hello world!</div>
);
}
}

import React from 'react';

export default class OtherPage extends React.Component
{
render()
{
return (
<div>Hi there! I am another page.</div>
);
}
}

We add the entry point (typically, app.js) to the mix:

import React from 'react';
import ReactDOM from 'react-dom';
import { renderToString } from 'react-dom/server';
import { match, RoutingContext } from 'react-router';
import createBrowserHistory from 'history/lib/createBrowserHistory';
import routes from './routes';

(function() {
'use strict';

if ('undefined' !== typeof document) {
// We're on the client, nothing new here
ReactDOM.render(
<Router
routes={routes}
history={createBrowserHistory({ queryKey: false })}
/>,
document.getElementById('app') // don't use document.body
);
} else {
// We're on the server, this will be executed by v8js
match(
{
routes,
location: uri
},
function(error, redirectLocation, renderProps) {
print(error
? error.message
: renderToString(<RoutingContext {...renderProps} />)
);
}
);
}
})();

You can see I used the same hack as in my previous article to tell apart client and server-side rendering routines. You can also see that on the server, the matched location used for routing comes from a variable named uri, that wasn't declared anywhere. To understand this, we must jump to index.php:

<?php

use Silex\Application;
use Silex\Provider\TwigServiceProvider;

require_once(__DIR__.'/vendor/autoload.php');

$app = new Application();
$app['v8js'] = new V8Js();

$app->register(new TwigServiceProvider(), ['twig.path' => __DIR__.'/views']);

$app
->get(
'/{uri}',
function ($uri) use ($app) {
$js = implode(';', [
'var uri = \'/'.$uri.'\'', // inject URI
file_get_contents(__DIR__.'/dist/app.js')
]);

ob_start();
$app['v8js']->executeString($js);
$content = ob_get_clean();

return $app['twig']->render('app.twig', compact('content'));
}
)
->assert('uri', '.*')
->value('uri', '');

$app->run();

We end with our Twig template:

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<title>Test</title>
</head>

<body>
<div id="app">{{ content|raw }}</div>
<!-- This script tag is the reason we needed
a container inside the body -->
<script src="dist/app.js"></script>
</body>

</html>

In order for this to work, your browser must support the History API, and you must use a web server such as Apache configured in the likes of this:

<IfModule mod_rewrite.c>
Options -MultiViews

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]
</IfModule>

You can then try out server-side rendering by commenting out the script tag in your template, in order to notice your React code truly is rendered by PHP! Navigate to /other and you will see the alternative content show up.

Your app can now be browsed with and without JavaScript!