undo
Go Beyond the Code
arrow_forward_ios

Representing UI state in the URL with React and Hooks

December 19, 2023

Introduction

Modern SPA frameworks like React in which the whole UI "runs" in the browser can manage practically the entire state within the app itself, mutating the UI as needed to reflect interaction and hitting a backend only when required. Among other things, that means that the entire interaction, including navigation between screens, can be done without any change in the URL - for instance, everything can be under a "/app" path. However, by using proper routing frameworks and by following the good practice of representing the current UI State In The URL (SITU from now), we can get several benefits like

  1. Bookmarking and Sharing: Users can easily bookmark or share specific screens in the current state we are seeing them. This allows them to revisit or share the same content and state, filter conditions, etc.
  2. Improved Usability: Users can directly access filtered views by simply opening a bookmarked URL. This eliminates the need for navigating through multiple pages or configuring filters repeatedly, providing a seamless and efficient user experience.
  3. Improved navigation: Since every important state change is view as a navigation from the browser, native back and forward functionalities are natural and seamless
  4. Better SEO, since Googlebot and other modern web crawlers that interpret Javascript can understand the navigation structure of the application and of course consider the URL as part of the indexing process.

Let's use the following mockups as an example of an e-commerce website that does not implement the SITU pattern. As we can see from the pictures, all of the views run under the "/products" path, which is something that we can easily do in React using libraries like react-router-dom and then managing the rest of the state by using the useState hook. Basically none of these views show the advantages explained above. So, let's see how we can improve this.

Figure 1. List of products with no filtering

Figure 2. List of products filtered to show only the products that match the "Electronics" category

Figure 3. Product detail view

First improvement approach

We can improve the current app implementation by doing the following:

  • Define a URL structure that can accommodate filter conditions in the product list view. For example:

https://example.com/products?category=electronics&price=100-200

  • Extract filter conditions from the URL using the URLSearchParams API provided by the browser. Use the get() method to retrieve individual filter values from the query parameters.
  • When filters are applied or modified, update the URL using the history object provided by the react-router-dom library. Use the push() method to update the URL with the new filter values.

Now let's see how we can implement this in Re


In this code stub:

  • We use the useHistory hook from react-router-dom to access the history object, which allows you to navigate and update the URL.
  • The applyFilters function takes an object containing the filter conditions and converts them into a URL query string using URLSearchParams. It then uses history.push() to update the URL with the new filter values.


Now, let's see how the URLs for the previous example will look like after this change

Figure 4. Updated product list view: the URL is the same in this case

Figure 5. List of products filtered to show only the products that match the 'Electronics' category and have a price in the range of $1200-$1400 with the filters present in the URL

Figure 6: Product detail view with the product id present in the URL

Now we have the SITU pattern implemented but there is a catch: we need to write the same code stub manually for each component. Let's check how we can generalize this.


Implementing a hook for simplifying the URL handling 

In order to implement the improvement we are gonna take advantage of one of the most important React mechanisms: hooks. This way we’ll be able to reduce the amount of code needed to leverage the benefits of extracting the filter conditions to the URL. We are going to call the hook useUpdateUrl.

Below you can find the source code of the hook and some marks in the code that we are going to explain one by one.


  1. Here we ensure to handle the component life cycle properly. We create a reference (isMountedRef) using the useRef hook to keep track of whether the component is mounted or not. This is used to ensure that URL updates are skipped if the component is unmounted during the update process.
  1. We create the updateURL function, which is the one that will perform the update in the URL that will be returned by the hook. This function will handle updating the URL with the provided pathname and searchParams. It checks if the component is still mounted before updating the URL to avoid potential memory leaks or errors when updating state on unmounted components.
  1. We ensure that we perform the initial URL update based on the current state. We use another useEffect hook to perform the initial URL update with the default values (if provided) when the component mounts.

Some extra notes about the implementation:
  • The useUpdateUrl hook provides the updateUrl function, which can be used to update the URL with new filter conditions.
  • The hook utilizes the useHistory and useLocation hooks from react-router-dom to access the history object and current location.
  • The updateUrl function takes a pathname and searchParams as parameters and uses the history.push() method to update the URL.
  • The hook ensures that updates to the URL are skipped if the component is not mounted using the isMountedRef ref.


Results

Let's see how we can apply the good practice of representing state in the URL in a very succinct and simple way in a concrete example. Below there is the original code without the hook.

Now with the hook, we can have the exact same behavior just by writing this:

Conclusion

The SITU pattern has several advantages: bookmarking, browsing, SEO, among others. When using React, by encapsulating the logic for handling URL parameters within a custom hook (for ultimately implement the SITU practice itself), we promote the concept of "single responsibility" a key tenet of clean code. The hook's purpose becomes laser-focused on this specific task, keeping our codebase concise, easy to comprehend, and less prone to bugs. This modular approach enables us to separate concerns and maintain a more organized and maintainable code structure.

Additionally, leveraging the custom hook encourages the reuse of code, another essential principle of clean code. Rather than scattering URL parameters handling logic across various components, we consolidate them within the hook, allowing other parts of our application to use them effortlessly. This reduces redundant code and promotes a more efficient development process.

Juan Altamirano
Software Engineer & Solver

Start Your Digital Journey Now!

Which capabilities are you interested in?
You may select more than one.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.