Rendering React with PHP, part two: semantic response codes

If you've read my last article, you now know how to render React code in a PHP app in order to serve the same content for the same URL regardless of how we navigate to it (hitting the server or interacting in the client). Today, we are going to build upon last time's example in order to make our Silex app yield semantic HTTP response codes based on what react-router says: 302s for redirects, 404s for inexistant URLs and 500s for errors.

Let's start by modifying our PHP code:

<?php

use Silex\Application;
use Silex\Provider\TwigServiceProvider;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

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) {
// Define content markers for special cases
$redirectPrefix = 'redirect=';
$notFound = 'notfound';
$errorPrefix = 'error=';

$js = implode(';', [
'var uri = \'/'.$uri.'\'',
// Inject them to be able to use them on the JS side
'var redirectPrefix = \''.$redirectPrefix.'\'',
'var notFound = \''.$notFound.'\'',
'var errorPrefix = \''.$errorPrefix.'\'',
file_get_contents(__DIR__.'/dist/app.js')
]);

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

if (preg_match('/^'.$redirectPrefix.'/', $content)) {
// JS-generated response is "redirect=/new/uri"
return new RedirectResponse(substr($content, strlen($redirectPrefix)));
}

if ($notFound === $content) {
throw new NotFoundHttpException();
}

if (preg_match('/^'.$errorPrefix.'/', $content)) {
// JS-generated response is "error=Some error message"
throw new RuntimeException(substr($content, strlen($errorPrefix)));
}

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

$app->run();

We will now make our JavaScript code craft appropriately shaped responses to make it all work together:

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) {
// client-side
// ...
} else {
match(
{
routes,
location: uri
},
function(error, redirectLocation, renderProps) {
print(error
? errorPrefix + JSON.stringify(error)
: redirectLocation
? redirectPrefix + redirectLocation.pathname + redirectLocation.search
: renderProps
? renderToString(<RoutingContext {...renderProps} />)
: notFound
);
}
);
}
})();

And it works! To try it out, you can add a redirect to your routes as follows:

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
},
{
path: 'redirect',
onEnter: function (nextState, replaceState) {
replaceState(null, '/');
}
}
]
};

You obviously also can use react-router's JSX notation, if you prefer to do so.