Writing a custom react hook that persists to local storage
Tags: react, hooks, javascript, reacttestinglibrary
Published: 2019-04-05 21:34:04

---
title: Writing a custom react hook that persists to local storage
published: true
description: How to write a custom hook that persists to local storage
tags: react, hooks, javascript, react-testing-library
---

So, you're writing some react hooks, and you say to yourself, "I'd love for this local state to persist on refreshes".

Let's write a custom hook that persists to localStorage!

## Setup

```bash
$ create-react-app local-storage-hook
$ cd local-storage-hook
$ yarn eject # accept all of the prompts
$ yarn add -D jest-localstorage-mock react-testing-library jest-dom
$ touch src/use-local-storage-set-state.js && touch src/use-local-storage-set-state.test.js && touch src/setupTests.js
```

Then, open up package.json, and edit the jest config,

add "jest-localstorage-mock" to the setupFiles section.

So now it looks like this:
```js
"setupFiles": [
   "react-app-polyfill/jsdom",
   "jest-localstorage-mock"
]
```
Then, add the following property to the jest config,

```js
"setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.js"
```


Make `src/setupTests.js` the following:

```js
import "react-testing-library/cleanup-after-each";
import "jest-dom/extend-expect";
```

(Before proceeding, if you are getting a weird error while running `yarn test`, then please do `rm -rf node_modules && yarn` and then this will solve those issues).

Alright! Let's write some tests!

```jsx
// use-local-storage-set-state.test.js
import React from "react";
import { useLocalStorageSetState } from "./use-local-storage-set-state";

test("throws when name is not provided", () => {
  expect(() => useLocalStorageSetState(0)).toThrow();
});
```

When running `yarn test`, this fails, so let's implement the source code:

```jsx
// use-local-storage-set-state.js
export const useLocalStorageSetState = (initialValue, name) => {
  if (!name) {
    throw new Error("Name must be provided to persist to localStorage");
  }
};
```

Now, when running `yarn test`, this test passes!

Unfortunately, this doesn't do much. Let's add extra tests to show what we're going for!


```jsx
// use-local-storage-set-state.test.js
import React from "react";
import { render, fireEvent } from "react-testing-library";
import { useLocalStorageSetState } from "./use-local-storage-set-state";

test("throws when name is not provided", () => {
  expect(() => useLocalStorageSetState(0)).toThrow();
});

test("persists on component unmounts and rerenders", () => {
  function Comp() {
    const [value, setValue] = useLocalStorageSetState(0, "value");

    return (
      <div>
        {value}
        <button onClick={() => setValue(value + 1)}>Add value</button>
      </div>
    );
  }
  const { getByText, rerender, unmount } = render(<Comp />);
  expect(getByText(/0/i)).toBeInTheDocument();
  fireEvent.click(getByText(/add value/i));
  expect(getByText(/1/i)).toBeInTheDocument();
});
```

Now let's add the source code:

```jsx
// use-local-storage-set-state.js
import React from "react";

export const useLocalStorageSetState = (initialValue, name) => {
  if (!name) {
    throw new Error("Name must be provided to persist to localStorage");
  }

  const [value, setValue] = React.useState(initialValue);

  return [value, setValue];
};
```

Now, when running `yarn test`, the tests pass!

Let's add more to the tests to show what more functionality we want, add the following:

```jsx
unmount();
rerender(<Comp />);
expect(getByText(/1/i)).toBeInTheDocument();
```

We're back to failing again! Let's add the proper source code.

Let's think about this before writing some random code.
When value changes, we want to persist that value into localStorage. So, value changes, function needs to fire.. This is exactly what useEffect is for!
Before we proceed, let's install the store npm module for efficient cross browser localStorage support:
`yarn add store`

Here is the source code with useEffect:
```jsx
// use-local-storage-set-state.js
import React from "react";
import store from "store";

export const useLocalStorageSetState = (initialValue, name) => {
  if (!name) {
    throw new Error("Name must be provided to persist to localStorage");
  }

  const [value, setValue] = React.useState(initialValue);

  React.useEffect(
    () => {
      store.set(name, value);
    },
    [value]
  );
  return [value, setValue];
};

```

`yarn test` is still failing, we're almost there! We need to read from localStorage for the initial value.

```jsx
// use-local-storage-set-state.js
import React from "react";
import store from "store";

export const useLocalStorageSetState = (initialValue, name) => {
  if (!name) {
    throw new Error("Name must be provided to persist to localStorage");
  }
  const actualInitialValue =
    store.get(name) !== undefined ? store.get(name) : initialValue;
  const [value, setValue] = React.useState(actualInitialValue);

  React.useEffect(
    () => {
      store.set(name, value);
    },
    [value]
  );
  return [value, setValue];
};

```

And now, `yarn test` is passing!

Now, there are a couple extra edge cases that we missed here, let me know in the comments if you would like those covered, but, you should be able to implement those yourself!

Source code available here: https://github.com/mcrowder65/local-storage-hook