While dealing with code in large and complex applications containing a large number of routes, we encountered an issue with the searchability of components and links corresponding to a given path.
Consider that we need to search for the component corresponding to the path "settings/general/users/a4cf-4efd-582a-38d3/manage-profile" in an application. But we don't know whether the path segment a4cf-4efd-582a-38d3 is referred to as :id, :user_id or with some other name in the codebase. Consequently, finding the corresponding <Route> or components with navigation to this route via <Redirect>, <Link>, etc., becomes a difficult task.
To overcome the above issue, we introduced a standard to keep all the routes in our application in a route.js file under the src directory as an object with static segments of the URL as the object path for the routes.
For example, if the route corresponding to the URL "settings/general/users/a4cf-4efd-582a-38d3/manage-profile" is "settings/general/users/:id/manage-profile", then it would be stored in the src/routes.js file as shown below along with all other routes in the application.
We will use the above route by importing the routes object and passing the object path for the route like this:
Defining routes like this enables us to search the occurrences of a route by typing in the static parts of the URL, which was difficult when we used strings and template literals. To search for the component or links corresponding to the URL "settings/general/users/a4cf-4efd-582a-38d3/manage-profile", we only need to search for the occurrences of settings.general.users.manageProfile in our editor.
Moreover, declaring and default exporting routes object provides us autocompletion for routes, making the development experience better:
Let's implement this in our project. First, create a routes.js file under src:
From the App component, you can see that our project has three routes: "/", "/products", and "/products/:slug". We need to organize them in the routes object. We will keep the root URL "/" as routes.root. Since the static part of "products" and "products/:slug" are the same, we will create two keys index and show inside the products key to distinguish between them. The index key indicates the listing route and the show key for the individual product route. Here is how the routes.js file will look like:
While defining the routes object, it is important to ensure that the object path matches the corresponding route. However, in our case, we have added extra keys to the object path to have unique keys for "/products" and "/products/:slug". Also, note that we have not included the slug as the object key to make the object path deterministic by avoiding the dynamic segments in the object path.
Now, replace the hardcoded routes in the App component:
Although we repeat the routes.products key here, we haven't destructured it as shown below.
Instead, we've used the complete object path. This choice is deliberate because our aim is to enhance the searchability of route usages, requiring the full object path.
Now that we have replaced all the hardcoded paths in the <Route> components, we should also replace the template string passed as a prop to the Link component. However, we should not store the template string `/products/${slug}`
into the route.js file.
Instead, we will create a utility function buildUrl to generate a URL from a string like "products/:slug" and an object containing dynamic values in a URL like { slug: "mens-cotton" }. We will pass the routes object path to this function. We should be able to use the buildUrl function in our ProductListItem component as shown below:
This approach allows us to look up the links to a path as we did for the <Route> components.
Now, let's define the buildUrl function. Given that this function will be utilized throughout the application, we'll place it under src/utils. Create a url.js file under src/utils for this:
The buildUrl function should accept a string representing routes with dynamic placeholders like :slug, :id, etc. Additionally, it should take an object containing key-value pairs representing the dynamic parts and their values. For instance, if our route includes :id, the params object would have a key id with its respective value, like this:
The buildUrl function will prepare the URL string with the dynamic value: /products/${slug}. However, instead of simple interpolation, we will encode the value using the encodeURIComponent function. This encoding ensures that any special characters within the provided parameters are converted into a valid URL format. For instance, if we pass a name parameter with the value "John Doe", it will be converted to "John%20Doe," where "%20" represents the UTF-8 code for space:
In addition to passing parameters as path segments, it's common to include query strings in the URL to send additional information. For instance, when a user is searching for a specific term in a search engine, the URL might look like this: www.google.com/search?q=bigbinary%20academy. In this URL, q=bigbinary%20academy is the query string, where q is the parameter for the search query and bigbinary academy is the value being searched.
To accommodate this, the buildUrl function needs to accept query parameters along with the params argument. As query parameters are passed with the params object, which also contains values for generating path segments, we will filter the keys used for path segments into an array called placeHolders and exclude them from the query parameters.
We'll adopt snake case for naming query parameter keys in the URL. So our next step entails converting the type case of queryParams object keys to snake case, utilizing the curried function keysToSnakeCase available in the neetocist library. Considering multiple transformations to be applied to the params object, we will employ pipe from Ramda to execute them sequentially.
Now that we have the valid query parameters as an object, we will create the query string using the stringify function from the qs package. The stringify function converts an object into a string of the form key1=value1&key2=value2 after encoding both keys and values. Finally, if queryParams is not empty, we will append it to the route followed by the ? symbol, adhering to the URL format.
Now, we can use the above function to generate URLs dynamically, passing the route and params. Pass the path prepared using buildUrl to the Link component in the ProductListItem component:
While this standardization may not seem to provide substantial value for our small project, given the effort involved, it significantly assists developers in navigating through the code within a large codebase.
Now, it's time to commit the changes. As we haven't made any functional changes to our application, it should continue to work as before.
You can verify the changes here.