Introducing Routing to a React Web App

A Noob Meets Web Apps, Episode 5

Marco Muccinelli

--

In previous episode I structured my React app using composition of components. Now it’s time to adopt react-router to go from poll list to single poll where user will vote.

$ npm install --save react-router-dom
$ npm install --save-dev @types/react-router-dom

Firebase hosting is already configured to support browser history thanks to global rewrites:

Make first route

I want to make a route to highlighted polls, parametrizing language. I move HighlightedPolls.tsx to app/features/highlightedPolls/component.tsx defining a new module. So I can create routeInfo.ts inside that folder:

If I can detect a valid language in route path I will load highlighted polls in that language; otherwise, I would like router to redirect to current browser language. To do so, I choose again composition by defining a I18n module exposing two components:

I18nFromMatch component uses I18n component to render a successfully matched language. When language can’t be identified, it renders via else() prop.

Now I can create app/features/highlightedPolls/route.tsx to encapsulate a <Route> valid to show requested polls:

Finally I can define BrowserRouter routes inside App.tsx. <Route> without path is rendered (and, so, it redirects) when first one fails to match, thanks to <Switch>:

Note that ‘en’ language is respected

It seems a little overkill to ha a separated RouteInfo object: I’ll definitely merge pattern informations inside HighlightedPolls.Route object before to go on.

Don’t forget who I am

I am a noob. While this approach works, it is not correct. <Switch> expects all children to be <Route> or <Redirect> components. Clearly HighlightedPolls.Route should not be put there: instead, I will define a function that returns a proper react-router route:

The mechanic is very similar, but I use HighlightedPollse.route() function to create an already configured <Route>.

Routing to poll detail

We need a component to display choices inside a poll. That will be the place where user could vote.

Firstly, we create a module in app/features/poll, like we have done with highlighted polls. We define a gateway to retrieve poll from API, a cell to display each choice detail and a main component that displays items in a grid:

Note that Columns is activated also on mobile devices and, in those environments, each column will be sized to 50% of available width.

I create also a route.tsx like I did for highlighted polls. But, first of all, I need to move <API> and <I18nFromMatch> out of HighlightedPolls.route(). That’s because also Poll.route() will need that data, so it’s preferable to move this state up.

I18FromMatch can be easily substituted by I18n.route():

HighlightedPolls.route() can now be streamlined to a very simple function:

The same concept can be applied to Poll.route():

Resulting routing structure in App.tsx is self-explainatory. Redirecting functions are just functions that return <Redirect>:

I18n.route() usage is a bit awkward because the double render prop. I can change approach by exporting a function that returns all routes that lead to a supported language. When URL contains an unsupported language <Switch> mechanics will render a <Redirect> to browser current language:

Drill down from polls to poll

The final step is to connect poll list to single poll detail. It should be trivial using <Link> component provided by react-router library. I’d like to make a step further by decoupling components from start: I don’t want Polls component to know nothing about routing to Poll.

I’ll introduce the generic concept of wrapping in features/polls/component.tsx:

HighlightedPolls still ignore existence of Poll component but it knows about routing, so I can use <Link> component:

Now I can add a function to features/poll/route.tsx in order to provide a way to generate route paths to reach single polls:

This function will be adopted inside App.tsx:

Note that poll path is generated using props.match, the match generated by I18n route. This is because we need to form an URL with `/:language/poll/:id` pattern, not`/:language/highlighted/poll/:id`

Links work and components are still decoupled: great!

Preserving Poll object

When you load Poll.route() you fire a fetch to download Poll object from API. This is a bit an overkill since we already have a Poll object stored inside Polls component.

We need to pass also Poll object, if available, from features/polls/component.tsx:

We need to provide a function to create link location in features/poll/route.tsx. When you find a poll inside location state, it is not necessary to fetch data from API.

Adaptation to features/highlightedPolls/component.tsx is straightforward. Finally we just need to make HighlightedPolls.route() to use Poll.locationDescriptor():

It remains only to merge everything in App.tsx, in route definitions:

Now, when you click on a poll, you are routed to detail without fetching; if you come directly (e.g.: by pasting URL), fetch is performed as before.

--

--