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
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
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.
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
.
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.
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.
const setValue = useCallback(
(value: T) => {
localStorage.setItem(name, JSON.stringify(value));
setState(value);
},
[name]
);
Step 3
Let's write the get logic.
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
const [state, setState] = useState<T>(getFromStorage());
Now it should look like this
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
.
return [state, setValue];
Now the final code should be as such
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
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
.
You can also use the usePersist in Context API.
I am using it for changing between the dark mode and the light mode.
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.
Top comments (0)