Borak - software developer illustrational
Blog
2.5. 2021 / 02:53

javascript

front end

library

React

State Management

Redux

URL state in React Applications

Let's think about how to develop your own solution for URL state of your application. This is introductory article, that will be followed by tutorial.
There are many tools for handling the state in React SPA applications. The URL state however seems to be rather left behind.

If you do a quick research, you will see, there are some libraries approaching the URL state handling. But the functionality they offer is mostly bound to ad hoc use cases, that don’t have to be acceptable for your individual requirements.

When I was looking for the solution for our business requirements, those offered by recent libraries were often close to our needs. But never close enough.

One of the closest solutions was the Spotify’s one called RLS.

https://github.com/spotify/redux-location-state

The behaviour was close to what I needed. But still little different. Not applicable to my requirements.

So, I looked at the license (https://github.com/spotify/redux-location-state/blob/master/LICENSE) and set off to write my own solution based on its codebase.
 
Inspired by already present solutions, I started writing something, that fits the needs of my use case, but still generic enough, to keep the possibility of covering also other possible scenarios.

You can read more about individual types of the SPA state in this article
http://borakpetr.cz/blog/state-management-in-react-applications?lang=en

React-redux-url-state npm package

This library is supposed to work together with Redux and History libraries as peer dependencies.

It will check the search string at the time of the initial load of the application, handling the URL as single source of truth.

During the lifecycle of the application, it maps the parts of your Redux state, defined in the separate config, to the URL.

Which means, that it keeps the state projected to the URL and the resulted URL will set up the values in the state of your application, once you copy it and pass it to the browsers address bar.

Installation

npm install react-redux-url-state --save

Sample application using react-redux-url-state

You can get the sample application using this library here
https://github.com/PetrBorak/deep-url-app

After downloading, run the following to start the example SPA:

npm install
npm run start

Setup

Here is example of minimalistic setup of the store with the library


import { createBrowserHistory } from 'history';
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import {createReduxLocationActions, listenForHistoryChange } from 'react-redux-url-state';
import { paramSetup, mapLocationToState } from './init.deep.url';
import { reducer } from './store'
 
export const history = createBrowserHistory();
 
const { locationMiddleware, reducersWithLocation } = createReduxLocationActions(paramSetup, mapLocationToState, (history as any), combineReducers({
url: reducer
 }));
 
const middleware = applyMiddleware(locationMiddleware);
 
export const composeEnhancers = compose;
export const store = createStore(reducersWithLocation, composeEnhancers(middleware));
listenForHistoryChange(store, (history as any));

To start, you import the following exports from react-redux-url-state:
  • createReduxLocationActions
    • Creates the enhaced middleware and reducer that will be used to setup the Redux store
  • listenForHistoryChange
    • Will link the history library with the store
Then you import setup objects, that are used to setup the library.

  • paramSetup
    • Primary setup file for definition of which parts of the Redux state will be handled by the react-redux-url-state library
  • mapLocationToState
    • This is ordinary Redux reducer, which maps the changes in the url search string to state.
Now the react-redux-url-state library is setup and handling your URL as single source of truth.
Let’s Elaborate on the individual parts in more detail.

createReduxLocationActions

This function returns enhanced reducer and middleware that needs to be used for the setup of the Redux store. It accepts the following parameters:

  • paramsSetup
  • mapLocationToState
  • history
  • Original Store reducer

paramsSetup

This object is the setup of the linking between URL and Redux store. Let’s see what an example paramsSetup can look like


Const testPath = {
 "url": {
  "stateKey": "url.stateForTheUrl",
  "type": "number",
  "initialState": -1,
  "options": {"shouldPush": true}
  }
 }

export const paramSetup = {
 eventsToIgnore: [LOCATION_CHANGE],
 eventToMerge: MERGE_URL_T0_STATE_AND_HYDRATE_URL_FROM_STATE,
 '/test-path': testPath,
};


paramsSetup - eventToIgnore property

You can setup events, that will be ignored by the react-redux-url-state middleware, by defining eventsToIgnore property of the paramsSetup object as an array of action type strings.

paramsSetup - eventToMerge property

This property defines an initial action at which the URL passed will be merged into the Redux state. It should be called from container component. This event also starts the process of mapping the defined Redux state to the URL.

paramsSetup definition of paths setup

The main functionality of paramsSetup is handled by properties whose key is used as mapper of the path in the URL.

In our paramsSetup we have defined

'/test-path': testPath,

That says to react-redux-url-state library to use the definition defined in the testPath object for URL with path

“/test-path”

Let’s look at testPath config object


Const testPath = {
"item": {
  "stateKey": "url.stateForTheUrl",
  "type": "number",
  "initialState": -1,
  "options": {"shouldPush": true}
   }
 }

The key, the “item” in our example defines that the item key in the URLs query part, will be handling some part of the state in the Redux.
The “item” config object includes the following:
  • stateKey
    • This is the path in the Redux state, which will be mapped to the URL’s query with key “item”
    • It has to include path from the root of the Redux state object down through the objects structure. 
    • In this particular case, the Redux store object can have the following structure
    • {
        url: {
         stateForTheUrl: 5
        }
      }
    • type
      • This tells the react-redux-url-state library, what is the type of the query parameter, that will be projected from URL to the Redux state
      • It is used for stringifying the Redux state value into URL string of the query and for parsing of the URL string to the Redux state
      • Available types are
        • array
        • number
        • boolean
        • string
        • object
          • As a flag option
          • As a record
    • options
      • This property allows to define some more specific traits of the library behaviour.
      • keepOrder
      • Available for array type. Is set to true, the library will preserve the order of its array items in the state according to order in the URL
      • parse
        • You can define you own parser, that will be used to transform values in the URL query into records in the Redux state
      • serialize
        • You can define you own serializer, that will be used to transform values in the Redux state to the URL queries
      • delimiter
        • symbol that will be used to separate the key from the value in the URL
        • or symbol that will be used to separate the items in the array projected to the URL
      • shouldPush
        • When the state changes, the library will map the state into query.
        •  If shouldPush is true, the resulted URL will be projected by calling history.push(URL)
          • Otherwise history.replace is used
      • isFlag
        • If used for record of type - Object. The items in the state will be projected as the Objects keys in the URL.
        • But only if the record for the key is true


mapLocationToState

This is pure function, which is used as a regular reducer. This reducer is called each time the URL changes.

In our example application, it looks like this:


export const mapLocationToState = (state: any, location: any) => {
  switch (location.pathname) {
  case '/test-path':
   return merge({}, state, location.query);
  default:
   return state;
   }
 };

It takes state and location as parameters. We define, what should be the resulted state based on these two parameters.


createReduxLocationActions – output

Let’s look at this function again.

createReduxLocationActions(paramSetup, mapLocationToState, history, combineReducers({url: reducer}));
It takes the
  • paramsSetup
  • mapLocationToState
  • history
  • original Redux reducer
And returns:

locationMiddleware and reducersWithLocation

  • locationMiddleware
    • is middleware that must be installed to Redux
  • reducersWithLocation
    • is original reducer enhanced by the library, that is handed over to Redux during setup

Creation of enhanced Redux store

On the last lines, we have

export const store = createStore(reducersWithLocation, composeEnhancers(middleware));
Here we create the Redux store and pass the enhanced reducer and react-redux-url-states middleware.


Linking with history library

The last thing, we need to do, is to link the whole react-redux-url-state engine to History library.
This is, what the last line in the example does:

listenForHistoryChange(store, (history as any));
We are done
Let’s talk about the common flow


URL state and common flow

When user copies the deep url to the Adress Bar, the engine needs to project it to the state.
When the user interacts with the application, the engine needs to project the observed state back to the URL.

The common flow starts always by entering the URL. However, some state changes can occur during the initial load of the application. At least the action of the Redux store to initialize to its initial values takes place in the beginning. We don’t want this initial state to overwrite our URL. The URL should be the source of truth.

For that reason, the react-redux-url-state library waits for signal, to start operating.
The signal is defined in the setup object:


export const paramSetup = {
 eventsToIgnore: [LOCATION_CHANGE],
 eventToMerge: MERGE_URL_T0_STATE_AND_HYDRATE_URL_FROM_STATE,
 '/test-path': testPath,
};

It’s the eventToMerge property. It’s simple string for the action creator. Before the library starts its magic, the action with the type defined by this property must be dispatched.
Before that, any change in the store can take place and the library will ignore it. It’s recommended for this action to be dispatched from some container component close to the root of the application.


Conclusion

Now we should be able to handle the URL in the application as a primary source of truth. Which means, propagating the state changes to the URL when needed and, at same time, creating stateful URLs, which can be used to preserve the state of the application and be reused by copying it.

What can be added? Well, it’s not even a library. This solution takes only 8 mid-sized files.

In am going to write a step by step tutorial, in which I will show you how to implement your own solution, similar to what is outlined in this article.














More by Borak

To maximalize your user experience during visit to my page, I use cookies.More info
I understand

#BORAKlive

This page is subjected to the Creative Common Licence. Always cite the Author - Do not use the page's content on commercial basis. Comply with the licence 3.0 Czech Republic.
go to top