Dynamic Server Side Routing with Redux
I have been working on a Java API Framework called light-4j. I encourage you to take a look at it. It is an amazing open source framework for creating API microservices.
While working with on this framework, I encountered an interesting use case. I needed to build a React application that receives routes from a server and then render an application with those routes.
A simple solution was to have an isomorphic app. With an isomorphic app, I can retrieve the routes in the server and then render the UI. However, for this use case we wanted to have a completely client side only application.
The routes will come from a service that only return the json structure for the routes.
What else could we do?
Since we cannot control the server and we only want to have a completely client side application, a solution is to make a HTTP request before rendering React.
Let me take a step back and talk about react-router. React Router is a powerful package that provides us with declarative routing. There are a couple ways to use this library.
- Dynamic Routing — not to be confused with dynamic server side routing
With this approach, you pass the <Route>
component the path and the component you want to render for that path.
const App = () => (
<div>
<nav>
<Link to="/dashboard">Dashboard</Link>
</nav>
<div>
<Route path="/dashboard" component={Dashboard}/>
</div>
</div>
)
It is simple to use and a great start for introducing routing to your application. However, since we are going to receive the routes from the server, we need a different approach.
- React Router Configuration.
If we read the docs for react-router-config, we see that one of the motivations to build that package was “loading data on the server or in the lifecycle before rendering the next screen”, which is exactly what we are looking for!
The first thing we need is a fallback route configuration. If the request fails we still want to render a working version of the application.
In the routes.js file, we declare all our routes to bootstrap the application.
/**
* Global Routes
*/
export const globalRoutes = [
{
component: Home,
routes: [
{
path: '/',
exact: true,
component: Home,
},
{
path: '/login',
component: Login,
},
{
path: '/admin',
exact: true,
component: Admin,
},
{
path: '/user',
component: User,
},
{
component: NotFound,
},
],
},
];
In our app.js, we are going to inject our default route configuration
import React from 'react';
import PropTypes from 'prop-types';
import { BrowserRouter as Router } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
const ContextType = {
// Redux store
store: PropTypes.object.isRequired,
routes: PropTypes.array.isRequired,
};
class App extends React.PureComponent {
static propTypes = {
context: PropTypes.shape(ContextType).isRequired,
};
static childContextTypes = ContextType;
getChildContext() {
return this.props.context;
}
render() {
return React.Children.only(
<Router>{renderRoutes(this.props.context.routes[0].routes)}</Router>,
);
}
}
export default App;
The magic happens inside the render function. We pass the routes array to renderRoutes and it will return a nested structure similar to the one on the dynamic routing section.
Now we can render the DOM with the routes we defined in the routes.js.
// @flow
import React from 'react';
import ReactDOM from 'react-dom';
import store from './store/configureStore';
import App from './components/App';
import { globalRoutes }from './routes';
/**
* Define context for Application
*/
const context = {
store,
routes: globalRoutes,
};
/**
* Mount Point
*/
const elementMountPoint = document.getElementById('root');ReactDOM.render(<App context={context} />, elementMountPoint);
We are half way there! So far we can render a routing structure from a centralized point, pretty cool.
However, for our use case, we are going to receive the routes from the server. We cannot inject the routes after rendering the DOM, it has to happen before.
Here is the magic:
(async () => {
ReactDOM.render(<ProgressBar />, elementMountPoint);
try {
const newContext = {
store,
routes: dynamicRoutes(context.routes, await fetchRoutes()),
};
ReactDOM.render(<App context={newContext} />, elementMountPoint);
} catch (e) {
ReactDOM.render(<App context={context} />, elementMountPoint);
}
})();
We replace the last line of app.js with the snippet above. Let me explain what is happening here.
We have a async function called fetchRoutes, this function simply makes an HTTP request to the server and returns a JSON array. This array is going to contain our route information.
In a async immediately-invoked function expression(IIFE), we have a try/catch. In the try, we set the routes to the response of fetchRoutes. Once we have the response, we pass those routes to the context and bootstrap the application.
However, if this request fails, the catch block gets executed and we render the default routes from our routes.js file.
Another cool feature is that we can show a progress bar before the UI gets rendered, that way users have something to look at.
I would love to know your thoughts to this solution. Did you find it helpful?
Till next time!