In my last blog post, I wrote about how I am using ReactJS as a template engine for spring boot. I am running the server side application on GraalVM, and my Spring controller uses ReactJS to render HTML. In the browser, the JavaScript will hydrate the components that have been rendered by the server.

This worked well for the simple example app that I had. But what about navigation? We need the same routing on the server side and on the client side, otherwise the server would render the wrong content when the user reloads a page.

And here’s how I did that with react-router…

You can find the whole project on Github: https://github.com/dtanzer/react-graalvm-springboot. The version of the code I describe in this blog post is 74a22861fcd263e19b31a17a81f808a580abad9c

react-router

First, I added react-router as a dependency to client/package.json:

"dependencies": {
    "@types/react-router": "^5.1.4",
    "@types/react-router-dom": "^5.1.3",
    "react-router": "^5.1.2",
    "react-router-dom": "^5.1.2",

And with react-router, I can do two things easily: Define which components to show at every given route and create a navigation that uses the router instead of directly linking to those URLs.

I created the component client/src/Routes.tsx to define the routes:

export const Routes = () => {
    return <>
        <Route path="/" component={ MainNavigation } />
        <Route path="/" exact component={ App } />
        <Route path="/r/about" component={ About } />
        <Route path="/r/list" component={ List } />
    </>
}

Those routes will always show the MainNavigation component (because the route “/” always matches), and then it will show one of the other components, App, About and List, depending on the route.

The three sub-components are dummy implementations for now. client/src/App.tsx, for example, contains this react component:

export function App(props: any) {
    return (
        <div>
            <h1>App</h1>
        </div>
    )
}

But client/src/MainNavigation.tsx contains more implementation - It links to the routes using the Link component from react-router:

export const MainNavigation = () => {
    return <ul>
        <li><NavLink to="/">Home</NavLink></li>
        <li><NavLink to="/r/about">About</NavLink></li>
        <li><NavLink to="/r/list">List</NavLink></li>
    </ul>
}

Routing on Server and Client Side

The Routes component from above must be inside a router component, otherwise the Route components from react-router do not work. On the client side, this is a BrowserRouter and on the server side, it is a StaticRouter. So, I configure that in client/src/index.tsx:

const anyWindow: any = window
anyWindow.renderApp = () => {
    ReactDOM.hydrate(<BrowserRouter><Routes /></BrowserRouter>, document.getElementById('root'))
}
anyWindow.renderAppOnServer = () => {
    return ReactDOMServer.renderToString(<StaticRouter location={anyWindow.requestUrl}><Routes /></StaticRouter>)
}
anyWindow.isServer = false

Here you can see that the StaticRouter needs a location parameter. I pass this parameter from the Kotlin code via the window object in server/src/main/kotlin/org/cloudicate/server/Controller.kt:

@GetMapping("/", "/r/**")
@ResponseBody
fun blog(request: HttpServletRequest): String {
    println(request.requestURI)
    engine.eval("window.requestUrl = '"+request.requestURI+"'")
    val html = engine.eval(renderJs)
    return indexHtml.replace("<div id=\"root\"></div>", "<div id=\"root\">$html</div>")
}

Why the /r/…?

Maybe you already saw it - all the routes (except for the “Home” link) start with /r/.

I needed that because spring boot will serve all files from server/public at /[file path]. But if I hade made the mapping of the controller @GetMapping("/**"), this would have overridden the files from public. The browser would not have been able to load static files anymore.

I needed a way to distinguish between static files and dynamic routes. And the easiest way I could think of was to prefix the dynamic routes with /r/.

To Recap…

Now I can use ReactJS as a template engine for spring boot and continue in the same react application as SPA in the browser. And I have the same routing on the server side and on the client side. So, if the user reloads the page, they get the same page rendered on the server side (and hydrated by react in the browser).

But one thing is still missing: How to get and send data? I will work on that in the next few days. Stay tuned!

Read / watch all parts of “Spring and Isomorphic React” here: