Sync React application state with the URL
Tags: react, javascript, webdev, beginners
Published: 2022-09-21 18:09:06

*A simplified approach to managing external states*

## TL;DR

- Check the github repo [here](https://github.com/codegino/react-sync-state-demo/blob/main/src/App.tsx)
- Jump <a href="#the-issue" target="_self">here</a> to see the issue were are trying to solve
- Final <a href="#final-solution" target="_self">code solution</a>

<br />

## Introduction

Wouldn't it be great if we can allow our application to store its state, and we can easily share it with others? For instance, on the GIF below, we can sort a table by the different properties; then, the URL will update accordingly. And the next time the same URL is loaded, the user will see the same view.

<br />

<Img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/agpd1sprwv4j7jsm0ofr.gif" />

> Of course, it could be more complex; instead of a table, use a card view filtered by the multiple properties and sorted by age in descending order, etc.

This idea is pretty common, but the question is, how do we implement it?

<br />


### What do we need to implement the feature?

Below are typical high-level steps to develop the requirement

1. Create an App component to store and load data
2. Create the Table component
3. Have a place to store the `sort` state
4. Handle updating the URL when a table header is clicked
5. Fetch the URL state, then update the component

> Steps 1 & 2 are probably the easiest, as they are our average react components.

#### Step 1: Create the plain version of the App that store and load data

```jsx
// App.jsx
const App = () => {
  const [data, setData] = React.useState([]);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(response => response.json())
      .then(json => setData(json));
  }, []);

  return (
    <div>
      <Table data={data} />
    </div>
  );
};

export default App;
```

<br />

#### Step 2: Create a simple table component

```jsx
// components/Table.jsx
const Table = ({data}) => {
  return (
    <table>
      <thead>
        <tr>
          <th>Username</th>
          <th>Name</th>
          <th>website</th>
        </tr>
      </thead>
      <tbody>
        {data.map(item => {
          return (
            <tr key={item.username}>
              <td>{item.username}</td>
              <td>{item.name}</td>
              <td>{item.website}</td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
};

export default Table;
```

<br />

### Complicated solution

Before going to the straightforward solution, let's see another way a React developer might do it.

<br />

#### Step 3: Store the `sortBy` state using the `React.useState` hook

 Instead of putting `sortBy` in the `Table` component, we lift the state from the table to the parent component to present data differently(i.e. Card View) 

```jsx
// App.jsx
const App = () => {
  const [sortBy, setSortBy] = React.useState('');

	// Create a sortedData based on the value of `sortProp`
  const sortedData = sortBy
    ? [...data].sort((a, b) => {
        const firstValue = a[sortBy];
        const secondValue = b[sortBy];

        return firstValue && secondValue
          ? firstValue.localeCompare(secondValue)
          : 0;
      })
    : data;

  return (
    <div>
      <Table data={sortedData} sortBy={sortBy} setSortBy={setSortBy} />
    </div>
  );
}
```


#### Step 4.1: Add table headers event handler

Add an event listener when a table header is clicked; then, propagate the event to the parent component.

```jsx
// components/Table.jsx
const Table = ({data, setSortBy, sortBy}) => {
  const handleSorting = (value) => {
    setSortBy(value === sortBy ? '' : value);
  };

  return (
    <table>
      <thead>
        <tr>
          <th onClick={() => handleSorting('username')}>Username</th>
          <th onClick={() => handleSorting('name')}>Name</th>
          <th onClick={() => handleSorting('website')}>website</th>
        </tr>
      </thead>
      {/* ... */}
    </table>
	)
}
```

#### Step 4.2: Saving the React-related state to the URL

Saving the state to the URL is just as simple as changing the URL itself. The way to change the URL depends on your routing library of choice. For the example below, I used the classic react-router.

```jsx
// components/Table.jsx
import { useNavigate, createSearchParams } from 'react-router-dom';

const Table = ({data, setSortBy, sortBy}) => {
  // react-outer utility hook to manage navigation
  const navigate = useNavigate();

  const handleSorting = (value) => {
    // Check if the new value is the active sortBy value
    const isActiveSort = value === sortBy;

    // Update sortBy in the React land
    setSortBy(isActiveSort ? '' : value);

    // Update sortBy in the URL land
    if (isActiveSort) {
      navigate({});
    } else {
      navigate({
        search: `?${createSearchParams({
          sortBy: value,
        })}`,
      });
    }
  };

  // return ...
}
```

#### Step 5: Loading State from URL

Before rendering our component, we retrieve the state of the URL and update the value of our `sortBy` state.

```jsx
// App.jsx
import {useSearchParams} from 'react-router-dom';

const App = () => {
  const [sortBy, setSortBy] = React.useState('');

  // React router utility hook to get the query params
  const [searchParams] = useSearchParams();

  useEffect(() => {
    const sortByQuery = searchParams.get('sortBy');

    setSortBy(sortByQuery);
  }, [searchParams]);

  // return ...
}
```

Now, it should be working as expected!

<Img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/obykv7s7ggll5aj08924.gif" />

<br />

#### But what if we have more queries to process?

If we follow the previous approach, our code might look like this.

```jsx
// App.jsx
const App = () => {
  const [sortBy, setSortBy] = React.useState('');
  const [sortByOrder, setSortByOrder] = React.useState('');
  const [filterBy, setFilterBy] = React.useState('');
  const [view, setView] = React.useState('table');
  const [focusedRow, setFocusedRow] = React.useState(-1)

  const [searchParams] = useSearchParams();

  useEffect(() => {
    const sortByQuery = searchParams.get('sortBy');
    const filterByQuery = searchParams.get('filterBy');
    const viewQuery = searchParams.get('view');
    const focusedRowQuery = searchParams.get('focusedRow');

    setSortBy(/* Derive sort field from sortByQuery */);
    setSortByOrder(/* Derive sort order from sortByQuery */);
    setFilterBy(filterByQuery);
    setView(viewQuery);
    setFocusedRow(focusedRowQuery);
  }, [searchParams]);


  const sortedData = // process sorted data

  return (
    <div>
      <Table data={sortedData}
        sortBy={sortBy} setSortBy={setSortBy}
        sortByOrder={sortByOrder} setSortByOrder={setSortByOrder}
        filterBy={filterBy} setFilterBy={setFilterBy}
        view={view} setView={setView}
        focusedRow={focusedRow} setFocusedRow={setFocusedRow}
      />
    </div>
  );
}
```

You might say that we can just use a `useReducer` or some state management, but it does not address the root of the issue.

<br />

### What is the issue were are trying to solve?  <a name="the-issue"></a>

The problem is creating two sources of truth: our React state and the URL state. The worse part is if we update those states separately. If possible, follow the [DRY principle](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).

<br />

### Simplified solution

#### Step 3: Use the actual URL object as the single source of truth

There is no need to create an additional state that we can track and modify. Simply derive the queries from the URL, and store the value in something immutable.

```jsx
// App.jsx
const App = () => {
  // const [sortBy, setSortBy] = React.useState('');
  // const [searchParams] = useSearchParams();

  // useEffect(() => {
  //   const sortByQuery = searchParams.get('sortBy');
  //   setSortBy(sortByQuery);
  // }, [searchParams]);


  // This is the simplified version of Step 5
  const [searchParams] = useSearchParams();
  const sortBy = searchParams.get('sortBy') || '';

  // ...
}
```

#### Step 4(Simplified): Update the URL directly 

Our table component does not need to propagate state changes to the parent component.
By updating the URL directly, we can effectively revise our single source of truth.

```jsx
// components/Table.jsx
// const Table = ({data, setSortBy, sortBy}) => {
const Table = ({data}) => {
  const [searchParams] = useSearchParams();
  const sortBy = searchParams.get('sortBy');

  const handleSorting = (value) => {
    // Check if the new value is the active sortBy value
    const isActiveSort = value === sortBy;

    // setSortBy(isActiveSort ? '' : value);

    if (isActiveSort) {
      navigate({});
    } else {
      navigate({
        search: `?${createSearchParams({
          sortBy: value,
        })}`,
      });
    }
  };

  return (
    <table>
      <thead>
        <tr>
          <th onClick={() => handleSorting('username')}>Username</th>
          <th onClick={() => handleSorting('name')}>Name</th>
          <th onClick={() => handleSorting('website')}>website</th>
        </tr>
      </thead>
      {/* ... */}
    </table>
	)
}
```

Since we just need to update the URL to update the state, we can even leverage the default behavior of HTML to update our state.

```jsx
// components/Table.jsx
import {Link, useSearchParams} from 'react-router-dom';

const Table = ({data}) => {
  const [searchParams] = useSearchParams();
  const sortBy = searchParams.get('sortBy');

  // To avoid repetition in the `to` attribute
  const createToLink = (sortValue) => {
    return sortBy === sortValue ? '/' : `?sortBy=${sortValue}`;
  };

  return (
    <table>
      <thead>
        <tr>
          <th>
            <Link to={createToLink('username')}>Username</Link>
          </th>
          <th>
            <Link to={createToLink('name')}>Name</Link>
          </th>
          <th>
            <Link to={createToLink('website')}>Website</Link>
          </th>
        </tr>
      </thead>
      {/* ... */}
    </table>
  )
}
```


### Final Solution <a name="final-solution"></a>

```jsx
// App.jsx
import React, {useEffect} from 'react';
import {useSearchParams} from 'react-router-dom';
import Table from './Table';

const App = () => {
  const [data, setData] = React.useState([]);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(response => response.json())
      .then(json => setData(json));
  }, []);


  // Implementing full sorting behavior is not the focus of this blog
  // That is why the following code magically appears here
  const [searchParams] = useSearchParams();
  const sortBy = searchParams.get('sortBy') || '';
  const [sortProp, order] = sortBy ? sortBy.split(':') : [];


  const sortedData = sortBy
    ? [...data].sort((a, b) => {
        const firstValue = a[sortProp];
        const secondValue = b[sortProp];

        if (!firstValue || !secondValue) {
          return 0;
        }

        return order === 'desc'
          ? secondValue.localeCompare(firstValue)
          : firstValue.localeCompare(secondValue);
      })
    : data;

  return (
    <div>
      <Table data={sortedData} />
    </div>
  );
};

export default App;
```

### More about the single source of truth thingy

If we want to create a custom table header component, we don't need to deal with prop drilling because our single source of truth is in the URL itself.

> It's like we use the URL as Redux.

```jsx
// components/SortableTableHeader.jsx
const SortableTableHeader = ({sortValue, children, ...props}) => {
  const [searchParams] = useSearchParams();

  const sortBy = searchParams.get('sortBy');

  const [sortProp, order] = sortBy ? sortBy.split(':') : [];

  const createToLink = () => {
    if (sortValue === sortProp) {
      if (!order) {
        return `?sortBy=${sortValue}:desc`;
      } else {
        return '/';
      }
    } else {
      return `?sortBy=${sortValue}`;
    }
  };

  const sortSymbol = () => {
    if (sortValue === sortProp) {
      return !order ? '️⬇️' : '⬆️';
    }
    return '';
  };

  return (
    <th {...props}>
      <Link to={createToLink()}>{children}{sortSymbol()}</Link>
    </th>
  );
};
```

It just looks amazing how we could eliminate prop drilling.

```jsx
// components/Table.jsx
import React from 'react';
import {Link, useSearchParams} from 'react-router-dom';

const Table = ({data}) => {
  return (
    <table>
      <thead>
        <tr>
          <SortableTableHeader sortValue="username">Username</SortableTableHeader>
          <SortableTableHeader sortValue="name">Name</SortableTableHeader>
          <SortableTableHeader sortValue="website">Website</SortableTableHeader>
        </tr>
      </thead>
      <tbody>
        {data.map(item => {
          return (
            <tr key={item.username}>
              <td>{item.username}</td>
              <td>{item.name}</td>
              <td>{item.website}</td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
};

export default Table;
```

After applying the additional changes, we're back to our desired behavior

<Img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dglfh02qe0elpnrt60y9.gif" />

> This pattern can also be used for other states, such as app storage and server state(react-query); also with other frameworks.

## Conclusion

It's easy to miss a more straightforward solution because of the pattern that we are used to. The point here is not about avoiding the usage `useState`, `useReducer`, `redux`, etc. It is more on utilizing the web's default behavior to reduce our code's complexity.