How to persistently store state in React? [usePersist]
Published: 2022-06-01 18:20:59

useState is one of the basic hooks in React. But you can not keep your state persistent with useState. When the user refreshes the page, the state is gone. So **How do we keep persistent data/state in React?** We can write a custom hook that persists data.

## Show Me the Code
`usePersist.ts`
```typescript
import { useCallback, useState } from "react";

interface UsePersistProps<T> {
	stateName: string;
	initialValue: T;
}

const usePersist = <T>({ stateName, initialValue }: UsePersistProps<T>): [T, (value: T) => void] => {
	const name = `persist/${stateName}`;

	const getFromStorage = <T>(name: string, defaultValue?: T) => {
		try {
			const val = JSON.parse(localStorage.getItem(name) + "");
			if (val !== null) {
				return val;
			} else {
				localStorage.setItem(name, JSON.stringify(defaultValue));
			}
		} catch {
			return defaultValue;
		}
	};

	const [state, setState] = useState<T>(getFromStorage<T>(name, initialValue));

	const setValue = useCallback(
		(value: T) => {
			localStorage.setItem(name, JSON.stringify(value));
			setState(value);
			console.log(name, value);
		},
		[name]
	);

	return [state, setValue];
};

export default usePersist;

```

`Usage`
```typescript
const [persistedState, setPersistedState] = usePersist<string>({
	stateName: "myPersistedState",
	initialValue: "Hello World",
});
```

## How?
OK, the code above might look confusing. I might have messed up, or that may be the ideal solution for this specific task. You be the judge.

The custom hook saves the state in `localStorage` and returns it when needed. This is basically it. 

Let's rewrite it step by step to understand it better.

### Step 1
We have to give a name to save the data to the `localStorage`. We also may want to give an initial value to the custom hook like we do for `useState`. Like in `useState`, we also may want to know the type of the data that we are going to save. To do this, we can use generics.

```typescript
interface UsePersistProps<T> {
	stateName: string;
	initialValue: T;
}

const usePersist = <T>({ stateName, initialValue }: UsePersistProps<T>) => {
	const name = `persist/${stateName}`;

	const setValue = (value: T) => {};
};

export default usePersist;
```

### Step 2
Let's start writing the set logic. First, let's keep the data in `useState`.

```typescript
import { useState } from "react";

interface UsePersistProps<T> {
	stateName: string;
	initialValue: T;
}

const usePersist = <T>({ stateName, initialValue }: UsePersistProps<T>) => {
	const name = `persist/${stateName}`;
	const [state, setState] = useState<T>(initialValue);
};

export default usePersist;
```

Onto the set logic. As you might have guessed, we save the data to the `localStorage`. But I also want to save the data to the `useState`. This way, we won't have to read the data from the localStorage to return the data.

```typescript
const setValue = (value: T) => {
	localStorage.setItem(name, JSON.stringify(value));
	setState(value);
};
```

It's pretty straightforward, right? However we will have an infinite render loop issue if we don't wrap this inside `useCallback`. React doesn't know if the `setValue` function will change or not. But we do. We might skip adding the function to the dependency array when we use it inside `useEffect` but eslint will annoy us.

Further readings:
https://reactjs.org/docs/hooks-reference.html#usecallback
https://github.com/facebook/react/issues/14920

Let's wrap it inside the `useCallback` hook and pass the `name` dependency even we know that we won't change the name.

```typescript
const setValue = useCallback(
	(value: T) => {
		localStorage.setItem(name, JSON.stringify(value));
		setState(value);
	},
	[name]
);
```

### Step 3
Let's write the get logic.

```typescript
const getFromStorage = () => {
	try {
		const val = JSON.parse(localStorage.getItem(name) + "");
		if (val !== null) {
			return val;
		} else {
			localStorage.setItem(name, JSON.stringify(initialValue));
		}
	} catch {
		return initialValue;
	}
};
```

Basically, we are trying to get the data from the `localStorage`. If the data does not exist, then we save it to the `localStorage`. The code is wrapped inside the try-catch block just in case if the data cannot be parsable. If happens so, the code returns the `initialValue`.

### Step 4
Let's finalize the code

Put the `getFromStorage` function above the `useState`.
Pass the `getFromStorage()` function call to the `useState` as so
```typescript
const [state, setState] = useState<T>(getFromStorage());
```

Now it should look like this

```typescript
import { useCallback, useState } from "react";

interface UsePersistProps<T> {
	stateName: string;
	initialValue: T;
}

const usePersist = <T>({ stateName, initialValue }: UsePersistProps<T>) => {
	const name = `persist/${stateName}`;
	const getFromStorage = () => {
		try {
			const val = JSON.parse(localStorage.getItem(name) + "");
			if (val !== null) {
				return val;
			} else {
				localStorage.setItem(name, JSON.stringify(initialValue));
			}
		} catch {
			return initialValue;
		}
	};

	const [state, setState] = useState<T>(getFromStorage());

	const setValue = useCallback(
		(value: T) => {
			localStorage.setItem(name, JSON.stringify(value));
			setState(value);
		},
		[name]
	);
};

export default usePersist;
```

Now let's return the set and get functions like React does for `useState`.

```typescript
return [state, setValue];
```

Now the final code should be as such
```typescript
import { useCallback, useState } from "react";

interface UsePersistProps<T> {
	stateName: string;
	initialValue: T;
}

const usePersist = <T>({ stateName, initialValue }: UsePersistProps<T>): [T, (value: T) => void] => {
	const name = `persist/${stateName}`;

	const getFromStorage = () => {
		try {
			const val = JSON.parse(localStorage.getItem(name) + "");
			if (val !== null) {
				return val;
			} else {
				localStorage.setItem(name, JSON.stringify(initialValue));
			}
		} catch {
			return initialValue;
		}
	};

	const [state, setState] = useState<T>(getFromStorage());

	const setValue = useCallback(
		(value: T) => {
			localStorage.setItem(name, JSON.stringify(value));
			setState(value);
		},
		[name]
	);

	return [state, setValue];
};

export default usePersist;
```

## Usage
Let's use it inside a component

```typescript
function App() {
	const [persistentState, setPersistentState] = usePersist<string>({
		stateName: "myState",
		initialValue: "Hello World",
	});

	useEffect(() => {
		setPersistentState("Hello, I'm persistent");
	}, [setPersistentState]);

	useEffect(() => {
		console.log(persistentState);
	}, [persistentState]);

	return (
		<>
			<p>{persistentState}</p>
		</>
	);
}
```

You can verify it working by checking your developer console. You might also want to delete the entry in your `localStorage`.


![Screenshot of the developer console consists both localStorage screen and the console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9p7nnhgcjnsuejz37sgh.png)

You can also use the usePersist in Context API.
I am using it for changing between the dark mode and the light mode.

```typescript
import { usePersist } from "hooks";
import { createContext, FC, useEffect, useState } from "react";
interface DarkModeContextState {
	darkMode: boolean;
	setDarkMode: (darkMode: boolean) => void;
}

const contextDefaultValues: DarkModeContextState = {
	darkMode: true,
	setDarkMode: () => {},
};

export const DarkModeContext = createContext<DarkModeContextState>(contextDefaultValues);

const DarkModeProvider: FC = ({ children }) => {
	const [persistedDarkMode, setPersistedDarkMode] = usePersist<boolean>({
		stateName: "darkMode",
		initialValue: contextDefaultValues.darkMode,
	});
	const [darkMode, setDarkMode] = useState<boolean>(persistedDarkMode);

	useEffect(() => {
		setPersistedDarkMode(darkMode);
	}, [darkMode, setPersistedDarkMode]);

	return (
		<DarkModeContext.Provider
			value={{
				darkMode,
				setDarkMode: (val: boolean) => {
					setDarkMode(val);
				},
			}}
		>
			{children}
		</DarkModeContext.Provider>
	);
};

export default DarkModeProvider;
```

## Conclusion
Thanks for reading so far.

You might ask

"Why don't you use a package to handle this?".
We can, of course. But, I wanted to give an idea of how to solve a pretty basic problem. I prefer understanding the solution that I am using.

"Why don't we set to and get from the local storage right inside the component?"
That should work but I wanted to go with a more elegant solution.

If you have any more questions or any feedback, please let me know. Hopefully this might be a solution to your problem and/or give you an insight how to write a custom hook.