Scale and Manage Stateful URLs for Complex UI in React

Aditya Dewaskar
Aditya Dewaskar
5 min read
Posted on May 09, 2023
Scale and Manage Stateful URLs for Complex UI in React

You might have encountered complex dashboards comprising numerous paginated tables, where applying filters create custom views. A commonly occurring problem that developers often face is to manage the state of the filters, which is thereby sent in the API request to retrieve the filtered result. While some prefer to save it in the Global State/Context API, others prefer locally. This article is primarily focused on discussing a third approach that turns out to be more effective.

The following screenshot illustrates a complex table comprising multiple fields.

The following screenshot illustrates a filtration mechanism for creating custom views.

Saving in Global State/Context API

Saving data in the Global State should be your preferred choice only when you need to access it from somewhere else. If the filtered values are accessed from the same location where the API Requests are made, you should avoid saving data in the Global State.

The variable data needed for the API should always be passed from the location where it resides. In this case it’s the filter component setting up the filter state.

Saving in Local State

Managing complex objects in local state could be a hassle in React.

Your filter state could look like following.

const filter = {
  selectedCategories: [],
  selectedTags: [],
  selectedBrands: [],
  selectedCountries: [],
  selectedStatuses: [],
  searchQuery: ""
}

Manage this in the local state as per your preference using useState?

This can be far more complex as the nesting of the filter object could go any level depending on the user interface.

Another pitfall to this approach is the lack of data preservation in the URL. Let’s assume that after applying a few filters and adding search input you wish to share the URL with someone who could view the same results. This isn’t possible as the URL doesn’t really know about the state in which the snapshot of the table was really taken.

The Optimal Approach

When you save data in the URL using search params API, the filters being used onscreen also reflects in the URL, which would allow people to share it across teams for a blissful experience as the URL is stateful and has the filter data engraved into it.

But now the question is how should you save it? Should you simply convert the object into JSON using its Stringify method or rather take an alternate approach. This should be more of a personal choice, but directly saving a stringified object into the URL may not be visually acceptable.

Let's take a look at several JSON compressor techniques.

Zipson

Zipson is a very user-friendly and performant JSON compression library (both in runtime performance and the resulting compression performance). To compress your search params it requires you to run JSON Escape/Unescape and encode/decode them using Base64.

Base64

Base64 could be your preferred choice as you don't really need to install a separate library for encoding and decoding of the URL.

Using the Base64 approach makes the URL look a lot cleaner and the URL doesn't expose much information regarding its content.

Observing the Code

Being a frequent utility, it’s preferable to create a custom hook for it.

Here, we are using React Router V6 and React V7.

Let's create a custom React hook to manage all of these operations.

import { URLSearchParams } from 'url';
import { useSearchParams } from 'react-router-dom';

// Using Base64 strategy for JSON compressions, you can also use other techniques.
const PARSE = (x: string) => JSON.parse(atob(x)); 
const STRINGIFY = (x: unknown) => btoa(JSON.stringify(x));
const useFilter = (initialValues) => {
  const [searchParams,setSearchParams] = useSearchParams();
  const params = urlParamsToObject(searchParams);

  // This converts URL Search Params Object to a JSON Object
  const filter = useMemo(() => {
    return params.filter ? PARSE(params.filter) : initialValues;
  }, [initialValues, params.filter]);

  // Here we return a parsed value of the filter.
  const resetFilter = () => {
   // This resets the filter to initial values
   setFilter(initialValues);
  };
 const setFilter = (newFilterData) => {
    const stringifiedFilter = STRINGIFY({
      ...filter,
      ...newFilterData,
    });
    const updatedParams = {
      ...params,
      filter: stringifiedFilter,
    };
    setSearchParams(updatedParams);
  }
  return { filter, setFilter, resetFilter };
}

As shown above the hook returns filter, setFilter, and resetFilter.

  1. filter is a parsed version of the URL's filter search query param.

  2. setFilter is a method used to set the values of filter into the URL search param.

  3. resetFilter is used for resetting the filter values to the initialValues.

Here, we are also using a utility hook provided by React Router called useSearchParams. It works very similar to useState with a params object and setParams utility.

We also expect the initial values of the filter to set the default values in the URL.

This way just importing the hook in your component where the filters values are being set can add the data to your URL search params.

Now we can use this hook in the following manner:

function TableFilterComponent() {
  const {filter,setFilter,resetFilter} = useFilter({searchQuery: ''});
}

You can set the table filter values using the methods shown above.

Moreover, if your filter data is also being used somewhere else convert this to a singleton hook using the react-singleton-hook to avoid multiple instances, or else the filter would be reset every time.

Now, this is how the filter is engraved in the URL.

http://localhost:8081/ops/audience-all?filter=xxxxxxxxxxxxxxxxxx

Good luck managing complex UI with stateful URLs. Adios!