Routing-aware React server-side rendering on PHP with Silex and react-router
In a previous article, 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!