Mount a Multi-Page SPA into an App with Server-Side Routing
Tags: spa, react, routing, laravel
Published: 2023-06-02 23:58:26

---
title: Mount a Multi-Page SPA into an App with Server-Side Routing
published: true
date: 2023-06-02 23:50:01 UTC
tags: spa, react, routing, laravel
canonical_url: https://macarthur.me/posts/combining-client-server-routing
---

This is one of those posts largely written for later reference by my future self.

I've been building a small React application. It has a few routes orchestrated by React Router, but it needed to be mounted onto a path within a Laravel application that uses traditional server-side routing.

Here's a contrived scenario. Each router – server-side Laravel and client-side React – defines it's own set of routes, with the latter being mounted onto one route of the former. A similar setup could be seen with another router and framework (Vue, Django, whatever). I just happen to be working with Laravel and React.

![route diagram](https://www.macarthur.me/proxy-image/content/images/2023/06/image-2.png)

If the React application is _always_ booted up at the base route (in this case, `/spa`), there's no issue. React Router will completely assume ownership of subsequent navigations entirely on the client. But if the user mades a fresh request to one of these client-side routes directly, Laravel will attempt to resolve it as any other GET request, and it'll fail.

For example, say we have those routes defined like this, each rendering a specific Blade template:

```
Route::get('/server-rendered-one', function (Request $request) {
	return view('one');
});

Route::get('/server-rendered-two', function (Request $request) {
	return view('two');
});

Route::get('/spa', function (Request $request) {
	return view('spa');
});
```

Here's what you'll see by navigating to `/spa/client-rendered-two`:

![404 page](https://www.macarthur.me/proxy-image/content/images/size/w1600/2023/06/image-3.png)

That makes sense. That route doesn't exist, so Laravel behaves accordingly.

In order for these breeds of routes to live in harmony, **Laravel needs to surrender all direct requests for a route to the _same_ HTML** loading the _same_ React application, allowing the client-side router to take over with whatever path is shown in the browser.

## Tell React Router Where It'll Live

Here are the routes I've wired up with React Router. One base route with a couple of others:

```
import React from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter(
    [
        {
            path: "/",
            element: <>Root page!</>,
        },
        {
            path: "/client-rendered-two",
            element: <>Second client-rendered page!</>,
        },
        {
            path: "/client-rendered-three",
            element: <>Third client-rendered page!</>,
        },
    ]
);

const domNode = document.getElementById("root");
const root = createRoot(domNode);

root.render(<RouterProvider router={router} />);

```

In local development, navigating to `/` would expectedly render "Root page!" But in order the router to correctly parse the path while running on a server-rendered `/spa` path, I need to React Router know about it using the [`basename` option](https://reactrouter.com/en/main/routers/create-browser-router?ref=alex-macarthur#basename):

```
const router = createBrowserRouter(
	[
		{
			path: "/",
			element: <>Root page!</>,
		},
		{
			path: "/client-rendered-two",
			element: <>Second client-rendered page!</>,
		},
		{
			path: "/client-rendered-three",
			element: <>Third client-rendered page!</>,
		},
    ],
    {
		// Tell RR this app will always mounted at /spa.
		basename: "/spa", 
    }
);
```

With that in place, navigating to `/spa/client-rendered-two` will effectively instruct the router to ignore the base `/spa`, and to determine which component to rendered using the segments that follow. It'll also automatically prepend that path onto any client-side navigation that follows. So, we're good to go with the client-side piece.

## Catch All Path Parameters

In order to send any `/spa/*` request to the same Blade template, we're gonna have to do some "route globbing," which can be accomplished with Laravel's [optional parameters](https://laravel.com/docs/10.x/routing?ref=alex-macarthur#parameters-optional-parameters). The trick is to throw a `?` after the parameter you'd like to be optional:

```
Route::get('/spa/{whatevs?}', function (Request $request) {
    return view('spa');
});

```

That'll work for any two-segment paths, but it _won't_ work if you were to navigate to `/spa/taxation/is/theft`. To get the route to match any _any number of segments,_ we can use a [regular expression constraint](https://laravel.com/docs/10.x/routing?ref=alex-macarthur#parameters-optional-parameters):

```
Route::get('/spa/{whatever?}', function (Request $request) {
    return view('spa');
- });    
+ })->where('whatever', '.*');
```

This constraint uses the extremely loose `.*` pattern to tell Laravel that the `whatever` path segment can match _any_ type of character, even slashes, so we'll be covered if our React app every introduces deeper, more complex routes. Let's reload:

![working page](https://www.macarthur.me/proxy-image/content/images/2023/06/image-6.png)

All set. Laravel is ready to accept any request whose path starts with `/spa`, no matter what the rest of the URL looks like. And when it does, it's React Router's time to shine.

## Transferrable to Other Frameworks

Again, there's nothing conceptually here that limits mounting a client-side router into just Laravel. If you were working in Rails, for example, it's actually even simpler using a [wildcard segment](https://guides.rubyonrails.org/routing.html?ref=alex-macarthur#route-globbing-and-wildcard-segments). Just keep in mind that the parentheses are required – they make the segment optional:

```
# config/routes.rb

get 'spa/(*whatever)', to: 'spa#index'
```

And if you were using something like [Vue Router](https://router.vuejs.org/api/interfaces/RouterHistory.html?ref=alex-macarthur#Properties-base), you'd then set the `base` property for property configuring client-side routing. You get the idea. We're just dealing with JavaScript and HTTP stuff here. The tools are purely tactical.

## Why Tho

On the surface, using a pattern like this might feel over-engineered. Maintaining routes for an application can get complex in any single paradigm, and now we're distributing it over _two_ – server _and_ client? In this economy? Nevertheless, I've become more open to it as real-life scenarios have come up.

For one, I think it can be useful for "domain" management within a monolith application. This is roughly the situation around the project that prompted this post. The server can choose which server-side routes to expose (maybe one for each domain/part of the application), and allow the the client to then own the routes _within_ those domains. The entirety of the client-side experience can then be managed somewhat independently from the broader application.

Second, it can be helpful for slowly transitioning a traditional application over to a full client-side architecture. As more and more routing is delegated to the client, the application would manage fewer routes on its own, leaving only those for providing data and handling mutations.

For my own projects, however, I've been preferring a different mash of approaches – full server-side routing with a JS-powered front end that interfaces with those routes in a way that makes the application _feel_ like a full-blown SPA. It's been popularized by Inertia.js the past few years, and I'm using it to run [JamComments](https://jamcomments.com/?ref=alex-macarthur) with immense satisfaction.

At any rate, it's been rewarding to explore these varying models for routing, and I'm eager to see which ones lay the strongest claim for different use cases as time goes on.

—-

If you’d like to be notified whenever I publish a new post, consider signing up for [my newsletter](https://macarthur.me/newsletter).