In this article, we embark on a journey to construct an automatic routing system within a ReactJS application using react-router-dom and TypeScript. Our quest begins with the familiar starting point of any React project: Create React App. With its simplicity and efficiency, Create React App provides the perfect launching pad for our endeavors. We'll seamlessly integrate TypeScript into our project, leveraging its static typing capabilities to catch errors early in the development process and enhance code maintainability.
The desired results
RouteContainer.register({
path: '/',
element: <Store />,
title: "Boutique",
label: "Boutique"
})
RouteContainer.registreGroup({
path: '/products',
routes: [
{
path: '/best-selling',
element: <BestSelling />,
title: "Best Product Landing Page",
label: 'My Product'
},
{
path: '/newest',
element: <Newest />,
title: "Best Product Landing Page",
label: 'My Product'
},
{
path: '/:id',
element: <SingleProduct />,
title: "Single Product",
label: "Single Product"
},
]
})
This code will generate 4 routes as follow
/
=> Home page will render <Store />
component
/products/best-selling
=> will render <BestSelling />
component
/products/newest
=> will render <Newest />
component
/products/{id}
=> will render <SingleProduct />
component, and id
will be accessible from props.navigation
passed nativelly to the component.
Getting started
First, let's create a file (just a regular js file) where we will define our routes in the manner shown in the code snippet above. Let's assume that we already have four components: <Store />
, <BestSelling />
, <Newest />
, and <SingleProduct />
.
// File: src/Routes/routes.js
import RouteContainer from '../Routing/RouteContainer' // this is where we will handle the routing logic
// import your components here...
export default function () {
RouteContainer.register({
path: '/',
element: <Store />,
title: "Boutique",
label: "Boutique"
})
RouteContainer.registreGroup({
path: '/products',
routes: [
{
path: '/best-selling',
element: <BestSelling />,
title: "Best Product Landing Page",
label: 'My Product'
},
{
path: '/newest',
element: <Newest />,
title: "Best Product Landing Page",
label: 'My Product'
},
{
path: '/:id',
element: <SingleProduct />,
title: "Single Product",
label: "Single Product"
},
]
})
};
When the RouteContainer.register(RouteProps)
function is called, RouteContainer
will save the route data into a static array, which will be used later to dispatch routes. Let's first define some types.
//File: src/Routing/RouteContainer.ts
// This is the shape of route config (route data)
type routeConfig = {
path: string,
element: React.JSX.Element | React.ReactElement,
title: string | undefined,
label?: string
}
and crafting the first version of the RouteContainer
class.
//File: src/Routing/RouteContainer.ts
export default class RouteContainer {
static rawRoutes: routeConfig[] = []
static register (routeConfig: routeConfig): void {
RouteContainer.rawRoutes.push( routeConfig );
}
So now, the RouteContainer
is just using a static array
to hold our Route configs or data.
let's see how we are going to use this to render element based on route config.
for that we will create a dispach
function inside RouteContainer
.
//File: src/Routing/RouteContainer.ts
static dispatch () {
return RouteContainer.rawRoutes
}
in this function we are just returning the rawRoutes array, basically this array will be used by useRoutes from react-router-dom to render routes:
// File: App.js
function App() {
return (
<BrowserRouter>
<AppLauncher />
</BrowserRouter>
);
}
export default App;
// File AppLauncher.js
import { useRoutes } from "react-router-dom"
import RouteContainer from "./Routing/RouteContainer"
import registerRoutes from "../Routes/routes"
export default function RouteDispatcher () {
registerRoutes() // register routes in RouteContainer
return useRoutes([].concat( RouteContainer.dispatch() /** Dispatch registred route inside RouteRendrer */))
}
We can finally add registerGroup
to RouteContainer
:
//File: src/Routing/RouteContainer.ts
type routeGroupConfig = {
routes: Array<routeConfig>,
path: string
}
...
export default class RouteContainer {
...
static registreGroup ( routeGroupConfig: routeGroupConfig ) {
routeGroupConfig.routes.forEach(routeConfig => {
RouteContainer.register( {
...routeConfig,
path: RouteContainer.prepareAndMergePaths( routeGroupConfig.path, routeConfig.path )
} )
});
}
private static prepareAndMergePaths ( path: string, anOther: string ) {
return RouteContainer.preparePath( path ) + RouteContainer.preparePath( anOther )
}
private static preparePath(str: string) : string{
if( str.length === 0 ) {
return ""
}
str = str.endsWith('/') ? str.slice(0, -1) : str;
str = str.startsWith('/') ? str.slice(1, str.length) : str;
return str + "/"
}
Yeah, this was easy. but the question is why we are doing this?
I'm doing this to keep my routes organized in a single place, for example, in a routes.js
file, making it easy to remove, add, or update routes in my app. However, the best part is when each route needs specific contexts. If you find this helpful, comment, and I will post a new article on how to use this architecture to automatically inject providers
and layouts
into each route.
Top comments (0)