Migrating a project routes from legacy routes to typed-safe named routes
July 31, 2022 / 8 min read
Last Updated: July 31, 2022The 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
1import React from 'react'2import { Navigate, Routes, Route } from 'react-router-dom'3import Pages from '../pages'4import Splash from '../components/Blocks/Loading/Splash'5import PrivateRoute from './PrivateRoute'67const Router = () => {8return (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<Route15path='/dashboard-empty'16element={17<PrivateRoute18path='/dashboard-empty'19element={Pages.DashboardEmpty}20/>21}22/>23<Route24path='/profile'25element={<PrivateRoute path='/profile' element={Pages.Profile} />}26/>2728<Route29path='/onboarding'30element={31<PrivateRoute path='/onboarding' element={Pages.OnboardingNew} />32}33/>34<Route35path='/dashboard'36element={<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|-- src2| |-- api3| |-- assets4| | |-- fonts5| | |-- images6| | |-- styles7| |-- components8| | | |-- atoms9| | | |-- cards10| | | |-- forms11| |-- contexts12| |-- container13| |-- helpers14| |-- hooks15| |-- pages16| | |-- auth17| | | |-- components18| | | |-- contexts19| | | |-- hooks20| | | |-- pages21| | | |-- _index.tsx22| | | |-- root.tsx23| | |-- dashboard24| | |-- farms25| | |-- lands26| | |-- marketplace27| | |-- onboarding28| | |-- profile29| |-- routes30| | |-- privateRoutes.tsx31| | |-- router.tsx32| |-- 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.
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.- any context file within this folder doesn't leak out of this page.
- we may have different hooks but some of the hooks might be created for per page usage and we store them here.
pages/
folder within the auth any other page is the folder the child pages or sub-routes. eg: /farms/farm-details_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: /farmsroot.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.
1type Route<Params = void> = {2path: string;3link: Params extends void ? () => string : (params: Params) => string;4section: Links;5};67function createRoute<Params = void>(8path: string,9section: Links10): Route<Params> {11return {12path,13section,14// @ts-ignore15link: (params?: Params) => generatePath(path, params ?? {}),16};17}1819// prettier-ignore20const routes = {21root: createRoute('/', LINKS.ROOT),22token: createRoute<{token: string}>('auth/:token', LINKS.ROOT),23auth: createRoute<{token: string}>('auth', LINKS.ROOT),24logout: createRoute('logout', LINKS.ROOT),25// other routes26};2728type RouteId = keyof typeof routes;2930// Use with <Route />31// @ts-ignore32export const pathTo: Record<RouteId, string> = Object.fromEntries(33Object.entries(routes).map(([id, route]) => [id, route.path])34);3536// Use with <RouterLink />37// @ts-ignore38export const linkTo: {39[R in RouteId]: typeof routes[R]['link'];40} = Object.keys(routes).reduce((acc, key) => {41// @ts-ignore42// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access43acc[key] = routes[key].link;44return acc;45}, {});4647// For Analytics48export const sectionOf: Record<string, Links> = Object.fromEntries(49Object.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