Stateful React Pages using URLSearchParams and Local/Session Storage
Published: 2024-01-04 22:44:19

**Note: This article speaks to creating react web pages that can retain their active state after a refresh or link share through the use of URL params and react state.** 

## Table of content

- [What is a stateful page](#what-is-a-stateful-page)
- [Advantages of a stateful page](#advantages-of-a-stateful-page)
- [Methods to a stateful page madness](#methods-to-a-stateful-page-madness)
    - [Using URLSearchParams](#using-urlsearchparams)
        - [Setup the URLSearchParams](#setup-the-urlsearchparams)
        - [Retrieving URL values](#retrieving-url-values)
        - [Adding or Modifying the URL values](#adding-or-modifying-the-url-values)
        - [Retrieving values from URLSearchParams to restore state](#retrieving-values-from-urlsearchparams-to-restore-state)
    - [Using Local or Session Storage](#local-or-session-storage)
        - [Adding value to storage](#adding-value-to-storage)
        - [Retrieving values from the Storage API](#retrieving-values-from-the-storage-api)
        - [Removing or modifying values in storage](#removing-or-modifying-values-in-storage)
        - [Retrieving values from Storage to restore state](#retrieving-values-from-storage-to-restore-state)
    - [Alternatives state storage methods](#alternatives-state-storage-methods)
- [Helper implementations for URLSearchParams](#helper-implementations-for-urlsearchparams)
    - [Functions to manage URL search params](#functions-to-manage-url-search-params)
    - [Usage Examples](#usage-examples)
        - [Add some params entries](#add-some-params-entries)
        - [Invoke a callback after the URL has been updated](#invoke-a-callback-after-the-url-has-been-updated)
        - [Delete an entry from the URL params](#delete-an-entry-from-the-url-params)
        - [URLSearchParams to JSON object](#urlsearchparams-to-json-object)
        - [URLSearchParams to URL search string](#urlsearchparams-to-url-search-string)
- [Glossary](#glossary)
- [References](#references)

## What is a stateful page

First, what does stateful mean? According to [redhat](https://www.redhat.com/en/topics/cloud-native-apps/stateful-vs-stateless) stateful allows the users to store, record, and return already established information and processes over the internet. "Stateful applications and processes allow users to store, record, and return to already established information and processes over the internet". In the context of this article, stateful refers to the capability to store, record, and return information to a webpage after refreshing the page or the copied link to the page is open in a different browser or device. 

A stateful page will be able to render the UI and components as the last time the page information and processes are recorded even if the page is open in another browser or device.

## Advantages of a stateful page

- An advantage of making your pages and app stateful is giving the user the ability to resume on the page from their last interaction.
- Another reason to make the page stateful is to be able to share a URL with another user and they can get the exact presentation as shown on your end.
- For a page with a lot of data presentation it makes it sane to have a link to that presentation as it is, so the link can be revisited and still get your page.
- A stateful page can also allow auto-fill of form on a page even if the page refresh or internet connection is loss, hence the user can resume the filling of the from with their previously entered values still on the page.

## Methods to a stateful page madness

There are various ways to make a webpage stateful, two of the methods are storing the page state information in the URL params or using a local storage to store the page's current information. Both methods are described below:

### Using URLSearchParams

[React Router DOM](https://reactrouter.com/) provides an intuitive way of managing the URL search value of a webpage, it provides the [useSearchParams](https://reactrouter.com/en/main/hooks/use-search-params) hook that allows accessing the webpage URL search values and also changing the values without reloading the active webpage.

The useSearchParams hook provides the two variables the first for retrieving the URL search values, and the other for adding or modifying the URL search values.

#### Setup the URLSearchParams

The useSearchParams hook accepts a default search params value in the form of a string, Record, or URLSearchParams which will be useful in retrieving data from the webpage URL

Install the react-router-dom package

```bash
npm i react-router-dom
```

Initialize in your components

```ts
//...
import { useSearchParams } from "react-router-dom";
//...
const [searchParams, setSearchParams] = useSearchParams(window.location.search);
```

#### Retrieving URL values

An example is a webpage with the search params value: `?page=1&name=user-one`. The return value is either a string or null for a non-existence key.

```ts
const page = searchParams.get("page"); // 1
const name = searchParams.get("name"); // user-one
const size = searchParams.get("size"); // null
```

#### Adding or Modifying the URL values

The second value from the useSearchParams hooks the URL search values can be modified to add a new search param or remove or modify existing search params.

```ts
searchParams.delete("page"); // => ?name=user-one
searchParams.set("size", 10); // => ?name=user-one&size=10
searchParams.append("status",["APPROVED", "REJECTED"]); // => ?name=user-one&size=10&status=APPROVED&status=REJECTED
```

#### Retrieving values from URLSearchParams to restore state

```ts
// UrlParamsExamplePage.tsx
import React from 'react';
import { useSearchParams } from "react-router-dom";
import { Helpers } from './Helpers';

function UrlParamsExamplePage() {
  const [searchParams, setSearchParams] = useSearchParams(window.location.search);
  const [page, setPage] = React.useState<number>(parseInt(searchParams.get("page") ?? "0"));

  return (
    <div style={{ display: "flex", flexDirection: "column", width: "fit-content" }}>
      <span>Page is {page}</span>
      <button type='button' onClick={() => Helpers.updateSearchParams("page", page+1, setSearchParams, () => {
        setPage(page+1);
      })}>Next Page</button>
      <br/>
    </div>
  );
}

export default UrlParamsExamplePage;

```

Full source code at [UrlParamsExamplePage.tsx](https://github.com/quickutils/stateful-ui/blob/main/src/UrlParamsExamplePage.tsx)

### Using Local or Session Storage

The browser local or session storage can also be used to store a webpage state. The Browser's [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) implementations provide API to store, retrieve, delete and modify data for a website. Local storage is different from session storage in the sense that session storage only retains the stored data in that browser tab while local storage retains the data across tabs for that website, but both share identical APIs.

The local and session storage does not require any setup as it is readily available in the JavaScript global [window](https://developer.mozilla.org/en-US/docs/Web/API/Window) object.

#### Adding value to storage

```ts
const storage = window.localStorage ?? window.sessionStorage;
storage.setItem("page", "1");
storage.setItem("name", "user-one");
```

#### Retrieving values from the Storage API

The return value is either a string or null for a non-existence key.

```ts
const storage = window.localStorage ?? window.sessionStorage;
const page = storage.getItem("page"); // 1
const name = storage.getItem("name"); // user-one
const size = storage.getItem("size"); // null
```

#### Removing or modifying values in storage

The storage API provides the `removeItem` method to remove a key value, and also the `clear` method to remove all the entries for that website.

```ts
const storage = window.localStorage ?? window.sessionStorage;
storage.removeItem("page"); // delete a single entry
storage.clear();            // remove all entries for the website
```

#### Retrieving values from Storage to restore state

```ts
// StorageExamplePage.tsx
import React from 'react';

function StorageExamplePage() {
  const storage = window.localStorage ?? window.sessionStorage;
  const [page, setPage] = React.useState<number>(parseInt(storage.getItem("page") ?? "0"));

  return (
    <form style={{ display: "flex", flexDirection: "column", width: "fit-content" }}>
      <input placeholder="Name" defaultValue={storage.getItem("name") ?? ""} onChange={(e: any) => {
        storage.setItem("name", e.target.value);
      }}/>
      <span>Page is {page}</span>
      <button type='button' onClick={() => {
        storage.setItem("page", ""+(page-1));
        setPage(page-1);
      }}>Prev Page</button>
      <button type='button' onClick={() => {
        storage.setItem("page", ""+(page+1));
        setPage(page+1);
      }}>Next Page</button>
    </form>
  );
}

export default StorageExamplePage;
```

Full source code at [StorageExamplePage.tsx](https://github.com/quickutils/stateful-ui/blob/main/src/StorageExamplePage.tsx)

### Alternatives state storage methods

Other alternatives to store data to be used to restore React webpages to React native mobile page states are.

**Cache:** The browser cache can store data with expiry time and can also share the data with other websites if needed.
**Mobile - Async Storage:** React native provides an [async storage](https://reactnative.dev/docs/asyncstorage) library that can be used to store data between app closing and reopening.
**Mobile - [SQLite](https://www.npmjs.com/package/react-native-sqlite-storage):** Since the mobile app has access to IO operation on the persistence storage, compact DB like SQLite will be helpful in quick storage and retrieval of data for page state restoration.
**Mobile - File IO:** Another alternative for mobile is to use the mobile native file access capability to create a custom file on the device to store state data, and read the file to retrieve the state data.

## Helper implementations for URLSearchParams

{% collapsible Typescript's functions to manage URL params and examples on how to use the functions %}

### Functions to manage URL search params

```ts
import { SetURLSearchParams } from "react-router-dom";
// ...
export type NoseurObject<T> = { [key: string]: T; };
export type URLSearchParamsValue = string | string[] | number | number[] | undefined | null;
// ...
function updateSearchParams(key: string, value: URLSearchParamsValue, setSearchParams: SetURLSearchParams | URLSearchParams, cb?: Function | undefined) {
    const fun = (prev: URLSearchParams) => {
        if (value === undefined || value === null) {
            prev.delete(key);
        } else if (Array.isArray(value)) {
            prev.delete(key);
            for (const v of value) prev.append(key, `${v}`);
        } else {
            prev.set(key, `${value}`);
        }
        return prev;
    };
    if (setSearchParams instanceof URLSearchParams) fun(setSearchParams);
    else setSearchParams(fun);
    cb && cb();
}

function normalizeUrlParams(urlSearchParams: URLSearchParams, keyLookupMap: NoseurObject<string> = {}, extraParams: NoseurObject<string | number> = {}) {
    const params: NoseurObject<any> = {};
    Object.keys(Object.fromEntries(urlSearchParams)).forEach((key: string) => {
        const lookupKey = keyLookupMap[key] ?? key;
        const values = urlSearchParams.getAll(key);
        if (values.length) {
            params[lookupKey] = values.length > 1 ? values : values[0];
        }
    });
    Object.keys(extraParams).forEach((key) => params[key] = extraParams[key])
    return params;
}

function urlParamsToSearch(urlSearchParams: URLSearchParams, only?: string[], includes?: NoseurObject<URLSearchParamsValue>) {
    only && only.length && Object.keys(Object.fromEntries(urlSearchParams)).forEach((key: string) => {
        if (!only.includes(key)) urlSearchParams.delete(key);
    });
    if (includes) {
        Object.keys(includes).forEach((key: string) => updateSearchParams(key, includes[key], urlSearchParams));
    }
    return urlSearchParams.toString();
}
```

### Usage Examples

```ts
import { useSearchParams } from "react-router-dom";
//...
const [searchParams, setSearchParams] = useSearchParams(window.location.search);
```

#### Add some params entries

```ts
updateSearchParams("page", 1, setSearchParams);
updateSearchParams("name", "user-one", setSearchParams);
updateSearchParams("status", ["APPROVED", "REJECTED"], setSearchParams);
```

#### Invoke a callback after the URL has been updated

```ts
updateSearchParams("page", 2, setSearchParams, () => {
    // e.g. fetch data from page 2
});
```

#### Delete an entry from the URL params

```ts
updateSearchParams("name", null, setSearchParams);
updateSearchParams("name", undefined, setSearchParams);
```

#### URLSearchParams to JSON object

```ts
let search;
search = normalizeUrlParams(searchParams); // { "page": 2, "status": ["APPROVED", "REJECTED"] }

// with key lookup map
search = normalizeUrlParams(searchParams, { "page": "users_page" }); // { "users_page": 2, "status": ["APPROVED", "REJECTED"] }

// with extra params
search = normalizeUrlParams(searchParams, {}, {"size": 20, "name": "one"}); // { "page": 2, "status": ["APPROVED", "REJECTED"], "size": 20, "name": "one" }
```

#### URLSearchParams to URL search string

```ts
let searchString;
searchString = urlParamsToSearch(searchParams) // page=2&status=APPROVED&status=REJECTED&name=user-one

// only selected entries
searchString = urlParamsToSearch(searchParams, ["page", "name"]) // page=2&name=user-one

// include extra entry values
searchString = urlParamsToSearch(searchParams, [], {"size": 20}) // page=2&status=APPROVED&status=REJECTED&name=user-one&size=20

```

{% endcollapsible %}


## Glossary

- API: Application Programming Interface, a method, routine, or procedure exposed by a component to allow usage or modification of its functionalities.
- Stateful: The capability of a presentation or system to store, record, and return the system to its previous state.
- Stateless: The absence of the capability of a presentation or system to retain information from previous interactions.

## References

- https://github.com/quickutils/stateful-ui
- https://www.redhat.com/en/topics/cloud-native-apps/stateful-vs-stateless
- https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
- https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
- https://reactnative.dev/docs/asyncstorage
- https://stackoverflow.com/questions/44376002/what-are-my-options-for-storing-data-when-using-react-native-ios-and-android
- https://www.npmjs.com/package/react-native-sqlite-storage

The sample project of this article is hosted at https://quickutils.github.io/stateful-ui/ and the source code at https://github.com/quickutils/stateful-ui