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
- Advantages of a stateful page
- Methods to a stateful page madness
- Helper implementations for URLSearchParams
- Glossary
- References
What is a stateful page
First, what does stateful mean? According to redhat 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 provides an intuitive way of managing the URL search value of a webpage, it provides the useSearchParams 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
npm i react-router-dom
Initialize in your components
//...
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.
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.
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
// 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
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 and session storage 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 object.
Adding value to storage
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.
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.
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
// 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
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 library that can be used to store data between app closing and reopening.
Mobile - SQLite: 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
Typescript's functions to manage URL params and examples on how to use the functions
Functions to manage URL search params
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
import { useSearchParams } from "react-router-dom";
//...
const [searchParams, setSearchParams] = useSearchParams(window.location.search);
Add some params entries
updateSearchParams("page", 1, setSearchParams);
updateSearchParams("name", "user-one", setSearchParams);
updateSearchParams("status", ["APPROVED", "REJECTED"], setSearchParams);
Invoke a callback after the URL has been updated
updateSearchParams("page", 2, setSearchParams, () => {
// e.g. fetch data from page 2
});
Delete an entry from the URL params
updateSearchParams("name", null, setSearchParams);
updateSearchParams("name", undefined, setSearchParams);
URLSearchParams to JSON object
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
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
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
Top comments (0)