Offline storage for PWAs
Tags: react, webdev, pwa
Published: 2020-03-23 13:29:26

---
title: Offline storage for PWAs
published: true
date: 2020-03-04 18:45:58 UTC
tags: react,webdev,pwa
canonical_url: https://blog.logrocket.com/offline-storage-for-pwas/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/5kun0xrscp9wvter65ra.png
---
**Written by [John Reilly](https://blog.logrocket.com/author/johnreilly/)**✏️

No matter what type of application you’re building, you may want to store information that persists beyond a single user session. Sometimes (but not always), you want this information to live in some kind of centralized database. You may also want that data to be available if the user is offline so that even if they can’t connect to the network, they can still use the app to perform meaningful tasks.

To drive this capability, the app will likely require a considerable amount of data. How can we achieve this in the context of a [progressive web app (PWA)](https://dev.to/bnevilleoneill/from-create-react-app-to-pwa-4e7)?

## The problem with `localStorage`

If you were building a classic web app, you’d probably reach for [`Window.localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) at this point. `Window.localStorage` is a longstanding API that stores data beyond a single session. It has a simple API and is very easy to use. However, it presents a few key problems:

- `Window.localStorage` is synchronous, which is not a tremendous problem for every app, but could lead to issues if you’re building something that has significant performance needs
- `Window.localStorage` cannot be used in the context of a `Worker` or a `ServiceWorker` because the APIs are not available there.
- `Window.localStorage` stores only `string`s; while this isn’t a huge problem given [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) and [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse), it’s certainly an inconvenience

The second point here is a significant one. What do you do if you need to access offline data in the context of a `ServiceWorker` — which, if you’re working offline, you almost certainly will?

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## Where IndexedDB falls short

Fortunately, `localStorage` is not the only game in town. There’s an alternative offline storage mechanism available in browsers that goes by the curious name of [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). To quote the docs:

> IndexedDB is a transactional database system, like an SQL-based RDBMS. However, unlike SQL-based RDBMSes, which use fixed-column tables, IndexedDB is a JavaScript-based object-oriented database. IndexedDB lets you store and retrieve objects that are indexed with a key; any objects supported by the structured clone algorithm can be stored. You need to specify the database schema, open a connection to your database, and then retrieve and update data within a series of transactions.

It’s clear that IndexedDB is very powerful, but it sure doesn’t sound very simple. A further look at the [MDN example](https://github.com/mdn/to-do-notifications/blob/8b3e1708598e42062b0136608b1c5fbb66520f0a/scripts/todo.js#L48) of how to interact with IndexedDB does little to contradict that thought.

We’d like to be able to access data offline, but in a simple fashion — much like we could with `localStorage`, which has a wonderfully straightforward API. If only someone would build an abstraction on top of IndexedDB to make our lives easier…

Fortunately, someone did.

## IDB-Keyval to the rescue!

[Jake Archibald](https://twitter.com/jaffathecake) of Google created [IDB-Keyval](https://github.com/jakearchibald/idb-keyval), which bills itself as a “super-simple-small promise-based keyval store implemented with IndexedDB.”

The API is essentially equivalent to `localStorage` with a few lovely differences:

- The API is promise-based; all functions return a `Promise`, which makes it a nonblocking API
- Unlike `localStorage`, the API is not restricted to `string`s. According to the docs, it’s IDB-backed, meaning you can store anything structured-clonable, such as numbers, arrays, objects, dates, blobs, and more
- Because this is an abstraction built on top of IndexedDB, it can be used both in the context of a typical web app and also in a `Worker` or a `ServiceWorker` if required

## Basic use

To show how to use IDB-Keyval, we’re going to need an example application. We’ll demonstrate its basic functionality as well as how to use it in an application.

Let’s spin up a TypeScript React app with [Create React App](https://create-react-app.dev/):

```jsx
npx create-react-app offline-storage-in-a-pwa --template typescript
```

Next, add IDB-Keyval to it.

```jsx
yarn add idb-keyval
```

Update the `index.tsx` file to add a function that tests using IDB-Keyval.

```jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { set, get } from 'idb-keyval';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.register();

async function testIDBKeyval() {
    await set('hello', 'world');
    const whatDoWeHave = await get('hello');
    console.log(`When we queried idb-keyval for 'hello', we found: ${whatDoWeHave}`);
}

testIDBKeyval();
```

The `testIDBKeyval` function does the following:

- Adds a value of `'world'` to IndexedDB using IDB-Keyval for the key of `'hello'`
- Queries IndexedDB using IDB-Keyval for the key of `'hello'` and stores it in the variable `whatDoWeHave`
- Logs what we found

You’ll also note that `testIDBKeyval` is an `async` function. This is so that we can use `await` when we’re interacting with IDB-Keyval. Given that its API is `Promise`-based, it is `await-`friendly. Where you’re performing more than a single asynchronous operation at a time, it’s often valuable to use [async/await](https://dev.to/bnevilleoneill/async-await-in-typescript-3n6g) to increase the readability of your codebase.

What happens when we run our application with `yarn start`? Let’s do that and take a look at the devtools.

![Hello World App Using IDB-Keyval](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/03/hello_world_idb_keyval-1.png?resize=725%2C548&ssl=1)

We successfully wrote something into IndexedDB, read it back, and printed that value to the console.

## Using IDB-Keyval in React

What we’ve done so far is somewhat abstract. To implement a real-world use case, let’s create an application that enables users to choose between a dark mode and the regular display.

To start, we’ll replace our `App.tsx` with this:

```jsx
import React, { useState } from "react";
import "./App.css";

const sharedStyles = {
  height: "30rem",
  fontSize: "5rem",
  textAlign: "center"
} as const;

function App() {
  const [darkModeOn, setDarkModeOn] = useState(true)
  const handleOnChange = ({ target }: React.ChangeEvent<HTMLInputElement>) => setDarkModeOn(target.checked);

  const styles = {
    ...sharedStyles,
    ...(darkModeOn
      ? {
          backgroundColor: "black",
          color: "white"
        }
      : {
          backgroundColor: "white",
          color: "black"
        })
  };

  return (
    <div style={styles}>
      <input
        type="checkbox"
        value="darkMode"
        checked={darkModeOn}
        id="darkModeOn"
        name="darkModeOn"
        style={{ width: "3rem", height: "3rem" }}
        onChange={handleOnChange}
      />
      <label htmlFor="darkModeOn">Use dark mode?</label>
    </div>
  );
}

export default App;
```

When you run the app, you can see how it works.

![Dark Mode Option](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/03/use-dark-mode-1.gif?resize=725%2C144&ssl=1)

As you can see, this is implemented using React’s `useState` hook. Any user preference selected will be lost on a page refresh. Let’s see if we can take this state and move it into IndexedDB using `IDB-Keyval`.

We’ll change the code like so:

```jsx
import React, { useState, useEffect } from "react";
import { set, get } from "idb-keyval";
import "./App.css";

const sharedStyles = {
  height: "30rem",
  fontSize: "5rem",
  textAlign: "center"
} as const;

function App() {
  const [darkModeOn, setDarkModeOn] = useState<boolean | undefined>(undefined);

  useEffect(() => {
    get<boolean>("darkModeOn").then(value =>
      // If a value is retrieved then use it; otherwise default to true
      setDarkModeOn(value ?? true)
    );
  }, [setDarkModeOn]);

  const handleOnChange = ({ target }: React.ChangeEvent<HTMLInputElement>) => {
    setDarkModeOn(target.checked);

    set("darkModeOn", target.checked);
  };

  const styles = {
    ...sharedStyles,
    ...(darkModeOn
      ? {
          backgroundColor: "black",
          color: "white"
        }
      : {
          backgroundColor: "white",
          color: "black"
        })
  };

  return (
    <div style={styles}>
      {darkModeOn === undefined ? (
        <>Loading preferences...</>
      ) : (
        <>
          <input
            type="checkbox"
            value="darkMode"
            checked={darkModeOn}
            id="darkModeOn"
            name="darkModeOn"
            style={{ width: "3rem", height: "3rem" }}
            onChange={handleOnChange}
          />
          <label htmlFor="darkModeOn">Use dark mode?</label>
        </>
      )}
    </div>
  );
}

export default App;
```

Let’s outline the changes.

- `darkModeOn` is now initialized to `undefined` and the app displays a loading message until `darkModeOn` has a value
- The app attempts to load a value from IDB-Keyval with the key `'darkModeOn'` and set `darkModeOn` with the retrieved value. If no value is retrieved, it sets `darkModeOn` to `true`
- When the checkbox is changed, the corresponding value is both applied to `darkModeOn` and saved to IDB-Keyval with the key `'darkModeOn'`

This means that we are persisting preferences beyond page refresh in a fashion that will work both online and offline.

![Use Dark Mode With IDB-Keyval](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/03/use-dark-mode-with-idb-keyval-1.gif?resize=725%2C357&ssl=1)

## Using IDB-Keyval as a React hook

For bonus points, let’s move this functionality into a reusable React hook.

Create a new `usePersistedState.ts` file.

```jsx
import { useState, useEffect, useCallback } from "react";
import { set, get } from "idb-keyval";

export function usePersistedState<TState>(keyToPersistWith: string, defaultState: TState) {
    const [state, setState] = useState<TState | undefined>(undefined);

    useEffect(() => {
        get<TState>(keyToPersistWith).then(retrievedState =>
            // If a value is retrieved then use it; otherwise default to defaultValue
            setState(retrievedState ?? defaultState));
    }, [keyToPersistWith, setState, defaultState]);

    const setPersistedValue = useCallback((newValue: TState) => {
        setState(newValue);
        set(keyToPersistWith, newValue);
    }, [keyToPersistWith, setState]);

    return [state, setPersistedValue] as const;
}
```

This new hook is modeled after the API of [`useState`](https://reactjs.org/docs/hooks-reference.html#usestate) and named `usePersistentState`. It requires a key, which is the key that will be used to save the data. It also requires a default value to use in case nothing is found during the lookup.

Just like `useState`, it returns a stateful value and a function to update it.

Finally, let’s switch over our `App.tsx` to use our shiny new hook.

```jsx
import React from "react";
import "./App.css";
import { usePersistedState } from "./usePersistedState";

const sharedStyles = {
  height: "30rem",
  fontSize: "5rem",
  textAlign: "center"
} as const;

function App() {
  const [darkModeOn, setDarkModeOn] = usePersistedState<boolean>("darkModeOn", true);

  const handleOnChange = ({ target }: React.ChangeEvent<HTMLInputElement>) =>
    setDarkModeOn(target.checked);

  const styles = {
    ...sharedStyles,
    ...(darkModeOn
      ? {
        backgroundColor: "black",
        color: "white"
      }
      : {
        backgroundColor: "white",
        color: "black"
      })
  };

  return (
    <div style={styles}>
      {darkModeOn === undefined ? (
        <>Loading preferences...</>
      ) : (
          <>
            <input
              type="checkbox"
              value="darkMode"
              checked={darkModeOn}
              id="darkModeOn"
              name="darkModeOn"
              style={{ width: "3rem", height: "3rem" }}
              onChange={handleOnChange}
            />
            <label htmlFor="darkModeOn">Use dark mode?</label>
          </>
        )}
    </div>
  );
}

export default App;
```

## Conclusion

You should now have a solid understanding of how a web application or PWA can safely and easily store data that is persisted between sessions using native browser capabilities.

IndexedDB powered the solution we built in this tutorial. We used IDB-Keyval over IndexedDB for the delightful and familiar abstraction it offers. This allowed us to build a solution with a similarly lovely API.

It’s worth noting that there are alternatives to IDB-Keyval, such as [localForage](https://github.com/localForage/localForage), which would be a particularly good choice if you’re building for older browsers that may lack good IndexedDB support. But be aware that with improved backward compatibility comes larger download size. It’s important to make the tradeoffs that make sense for you.

Lastly, we illustrated how to use IDB-Keyval in a React context. Please note that there’s nothing React-specific about our offline storage mechanism. So if you’re rolling with Vue, Angular, or something else entirely, this tutorial could help you too.

Put simply, offline storage leads to better user experiences. You should consider using it in your applications.

* * * 
## Full visibility into production React apps 

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, [try LogRocket.](https://www2.logrocket.com/react-performance-monitoring)

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/eq752g8qhbffxt3hp9t4.png)

[LogRocket](https://www2.logrocket.com/react-performance-monitoring) is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — [start monitoring for free.](https://www2.logrocket.com/react-performance-monitoring)
* * *

The post [Offline storage for PWAs](https://blog.logrocket.com/offline-storage-for-pwas/) appeared first on [LogRocket Blog](https://blog.logrocket.com).