@sudocode_

Migrating a project routes from legacy routes to typed-safe named routes

July 31, 2022 / 8 min read

Last Updated: July 31, 2022

The Grower Dashboard presents a significant challenge due to its extensive network of routes, including nested routes. Complete Farmer's engineers face difficulties in organizing components within these routes, such as Modals and Tabs. As the product continues to expand, incorporating new pages and features becomes increasingly complex, posing scalability issues for the engineering team. However, the Grower Module Dashboard empowers farmers and investors to cultivate crops using data-driven protocols and access global markets to sell their produce competitively.

Recognizing the need for a better solution to address the scaling and routing challenges within the product, I embarked on a quest for improvement. How can we enhance the routing aspect and ensure that most features can be accessed through routes rather than relying on application state?

The Problem

The Grower Dashboard is a crucial tool utilized by our clients, particularly growers. However, when a grower encounters an issue and reaches out to our support team for assistance, they often face challenges in navigating the platform to locate the specific case. To alleviate this difficulty, an alternative approach is to provide support with a direct link to the reported issue. This would streamline the navigation process and make it more efficient.

As the product continues to scale, navigating the platform becomes increasingly complex due to the growing number of routes, including nested routes. As engineers, we constantly add new routes and make modifications to existing ones to accommodate the product's expansion. However, when a route undergoes changes, it requires thorough modifications across numerous components, resulting in significant challenges for our team.

This issue becomes particularly demanding at times, as the intricate task of updating routes in multiple components can be time-consuming and arduous for our engineers.

Sample routes we had in the past:

Routes.js

1
import React from 'react'
2
import { Navigate, Routes, Route } from 'react-router-dom'
3
import Pages from '../pages'
4
import Splash from '../components/Blocks/Loading/Splash'
5
import PrivateRoute from './PrivateRoute'
6
7
const Router = () => {
8
return (
9
<React.Suspense fallback={<Splash />}>
10
<Routes>
11
<Route path='/' element={<Navigate to='/auth' />} />
12
<Route path='/auth/:token' element={<Pages.Auth />} />
13
<Route path='/auth' element={<Pages.Auth />} />
14
<Route
15
path='/dashboard-empty'
16
element={
17
<PrivateRoute
18
path='/dashboard-empty'
19
element={Pages.DashboardEmpty}
20
/>
21
}
22
/>
23
<Route
24
path='/profile'
25
element={<PrivateRoute path='/profile' element={Pages.Profile} />}
26
/>
27
28
<Route
29
path='/onboarding'
30
element={
31
<PrivateRoute path='/onboarding' element={Pages.OnboardingNew} />
32
}
33
/>
34
<Route
35
path='/dashboard'
36
element={<PrivateRoute path='/dashboard' element={Pages.Dashboard} />}
37
/>
38
<Route path='/404' element={<Pages.NotFound />} />
39
<Route path='*' element={<Navigate to='/404' />} />
40
</Routes>
41
</React.Suspense>

The Solution

To simplify the process for both engineers and growers, our team collectively decided to focus on improving the project's structure and implementing enhanced routing within the application, all while prioritizing performance.

As the Lead Frontend Engineer, I took the initiative to begin restructuring the project by transitioning it from JavaScript to TypeScript. This switch allowed us to leverage the benefits of static typing and enhance the overall robustness of the codebase. Additionally, I embarked on thorough research to explore methods for incorporating type-safe named routes into our project. Although it presented some challenges along the way, I was determined to fulfill my role in supporting the team's objectives.

Project structuring looks like this:

Project Structure

1
|-- src
2
| |-- api
3
| |-- assets
4
| | |-- fonts
5
| | |-- images
6
| | |-- styles
7
| |-- components
8
| | | |-- atoms
9
| | | |-- cards
10
| | | |-- forms
11
| |-- contexts
12
| |-- container
13
| |-- helpers
14
| |-- hooks
15
| |-- pages
16
| | |-- auth
17
| | | |-- components
18
| | | |-- contexts
19
| | | |-- hooks
20
| | | |-- pages
21
| | | |-- _index.tsx
22
| | | |-- root.tsx
23
| | |-- dashboard
24
| | |-- farms
25
| | |-- lands
26
| | |-- marketplace
27
| | |-- onboarding
28
| | |-- profile
29
| |-- routes
30
| | |-- privateRoutes.tsx
31
| | |-- router.tsx
32
| |-- utils.ts

Let's go through the project structure; within the src folder, we have the api, assets, components, contexts, container, helpers, pages, routes, and utils folders respectively. I will explain what each folder contains and why I structure the project this way.

API folder

The api folder contains custom hooks created for querying and mutating data. These hooks are built on top on react query which makes it easier for our team to either query or mutate with data. It includes a single source of asynchronous function that works with any promise based method (including GET and POST methods) to fetch data from a server.

Assets folder

The assets folder contains three child folders(fonts, images, styles) for CSS files, and any other static files and data.

Components folder

This folder holds reusable components that are shared across the project. It contains subfolders like atoms that contain tiny/small reusable components, the forms folder contains any form inputs and the cards folder contains any card components that can be shared/used in the project.

Context folder

The context folder is the parent folder that contains any state management files created with Context API as we use React Context API mostly.

Container folder

This folder holds the layout of the project. Eg: Sidebar, Navbar, Footer, etc.

Helpers folder

This folder contains any miscellaneous functions or utilities that are reusable and shared across the project.

Hooks folder

As the name implies, this folder also contains custom hooks created within the project for reusability.

Pages folder

This folder contains all the pages for our routes. The page folder also has subfolders depending on the number of pages we have. Each page is a folder that contains subfolders as well. Eg: auth folder. Within the dashboard folder, as seen above, we have components, contexts, hooks, pages folders, _index.tsx and root.tsx.

  • ArrowAn icon representing an arrow
    components/ folder is for reusable components that are used only within the auth page. Any component within this folder isn't used in any part or any pages besides the auth page.
  • ArrowAn icon representing an arrow
    any context file within this folder doesn't leak out of this page.
  • ArrowAn icon representing an arrow
    we may have different hooks but some of the hooks might be created for per page usage and we store them here.
  • ArrowAn icon representing an arrow
    pages/ folder within the auth any other page is the folder the child pages or sub-routes. eg: /farms/farm-details
  • ArrowAn icon representing an arrow
    _index.tsx is for the page itself. In this file, we render the components or what the user see when they visit the page. eg: /farms
  • ArrowAn icon representing an arrow
    root.tsx is to map all the routes for this page. We want to be able to track all the routes for a specific page in one file. Since we deal with a lot of routes and sub-routes, it will be better if we can have all the sub-routes of a page in one file

Routes folder

The routes folder contains two (2) files; privateRoute.tsx and router.tsx. The privateRoute file is where we have our logic that checks if a user is authenticated and authorized to access certain parts of the dashboard or pages. The router file contains all our parent/root routes for the project. You will soon see what I mean by root routes and sub-routes within this project.

Utils folder

This folder contains and third-party utility file that's been modified by us. We mostly customize third-party tools/packages to suit our use cases. This contains utilities like our routing system which was modified from the react-router package.

Allow me to unveil our new routing system we created.

1
type Route<Params = void> = {
2
path: string;
3
link: Params extends void ? () => string : (params: Params) => string;
4
section: Links;
5
};
6
7
function createRoute<Params = void>(
8
path: string,
9
section: Links
10
): Route<Params> {
11
return {
12
path,
13
section,
14
// @ts-ignore
15
link: (params?: Params) => generatePath(path, params ?? {}),
16
};
17
}
18
19
// prettier-ignore
20
const routes = {
21
root: createRoute('/', LINKS.ROOT),
22
token: createRoute<{token: string}>('auth/:token', LINKS.ROOT),
23
auth: createRoute<{token: string}>('auth', LINKS.ROOT),
24
logout: createRoute('logout', LINKS.ROOT),
25
// other routes
26
};
27
28
type RouteId = keyof typeof routes;
29
30
// Use with <Route />
31
// @ts-ignore
32
export const pathTo: Record<RouteId, string> = Object.fromEntries(
33
Object.entries(routes).map(([id, route]) => [id, route.path])
34
);
35
36
// Use with <RouterLink />
37
// @ts-ignore
38
export const linkTo: {
39
[R in RouteId]: typeof routes[R]['link'];
40
} = Object.keys(routes).reduce((acc, key) => {
41
// @ts-ignore
42
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
43
acc[key] = routes[key].link;
44
return acc;
45
}, {});
46
47
// For Analytics
48
export const sectionOf: Record<string, Links> = Object.fromEntries(
49
Object.entries(routes).map(([, route]) => [route.path, route.section])
50
);

This routing system made it possible for us to put almost everything on our route. Just like on Remix.run, this takes the same approach but in a different way.

Our engineers are happy working with this typed-safe routes which makes development easier and quicker.

Liked this article? Share it with a friend on Twitter or contact me let's start a new project. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll always be available to respond to your messages.

Have a wonderful day.

– Felix

One thing I haven't spoken much about is how I got here. How did I get here