Persisting Data in React useState
When React introduced Functional Components, they quickly gained global popularity, primarily because of their simplicity and fluidity compared to Class Components. Functional Components (FC) are based on functions that React calls hooks. One of the most used hooks - if not the most used one - in React FC is the useState hook.
Understanding the useState Hook
The useState hook is used to manage the state of a React FC. It is a relatively simple JavaScript function that returns an array containing a getter and a setter for the state of the functional component.
React useState can be used to store data such as booleans, numbers and strings to more complex data structures such as arrays and objects. The getter is used to keep track of the current value, while the setter is used to update the state.
Here’s a simple example of a counter.
import { useState } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
const increment = () => setCount(count + 1)
const decrement = () => setCount(count - 1)
return (
<div>
<h1>Counter</h1>
<p>Current count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
export default Counter
Let’s unpack this. Here we are using useState to create a state that keeps track of a count value and updates that count value.
const [count, setCount] = useState(0)
The count variable above returns the current value of the counter, and the setCount function allows you to set a new value for the counter state. Finally, the state of the counter is initialized with a value of 0.
Below the counter useState, we have two helper functions, increment & decrement, which are pretty self-explanatory. The increment function increases the value of the state by one, and decrement reduces it by one.
The React FC then returns a simple div with a p tag showing the current count and two buttons responsible for activating the increment and decrement functions. When either button is clicked, the state of the component is updated, and the component is rerendered to show the updated state on the browser.
The problem.
The state of a React component is non-persistent. This means if I refresh the browser, the current state of the component is lost, and the state is re-initialized with a value of 0. This is not ideal.
Creating a usePersistentState Hook
Creating custom hooks is one of the most powerful tools in the arsenal of React FCs. We are going to make a custom hook that leverages the session storage of the browser to persist the component state on refresh.
function usePersistedState(key, defaultValue) {
const [state, setState] = useState(
() => JSON.parse(sessionStorage.getItem(key)) || defaultValue
)
useEffect(() => {
sessionStorage.setItem(key, JSON.stringify(state))
}, [key, state])
return [state, setState]
}
export { usePersistedState }
Let’s unpack. As you can see, this custom hook is leveraging the React useState hook. It accepts two arguments - key & defaultValue. The key is used to set and get our persisted state to and from session storage. The defaultValue argument is the value with which our state will be initialized. You will notice something different about how we initialize our state.
() => JSON.parse(sessionStorage.getItem(key)) || defaultValue
Instead of passing a value to initialize our useState hook, we pass a function that first checks if a value was stored in session storage using the key argument that was passed. Because session storage stores data as a string, we use JSON.parse() to convert it back to its original type (in our case, our counter is a number).
Here we are using a logical OR operator to check if a value is parsed from session storage and assigning defaultValue as the initializing value if no value is present in session storage for that key. The logical OR operator returns the first value if it is ‘truthy’; otherwise, it returns the second value if the first value is ‘falsy’. Examples of falsy values are
- null
- NaN
- 0
- empty string ("" or '' or ``) -undefined
Next, we are using another very essential React hook - useEffect. The useEffect hook is used to control the behaviour of our component. It can tell the component when to rerender by passing a dependency array, what logic it should perform when it is mounted and when it is being unmounted.
useEffect(() => {
sessionStorage.setItem(key, JSON.stringify(state))
}, [key, state])
In our useEffect, when the component mounts, we store our state in session storage using the key argument that was passed to the function. A dependency array is also given to the useEffect hook. The dependency array contains the key argument passed in and our state value. This means that whenever the key argument, or the state of our component changes, the useEffect hook will fire and reset our new state in session storage.
Finally, just like the React useState hook, our custom hook returns a tuple containing a setter and getter for our persisted state.
Let’s see it in use inside our counter.
import React from "react"
import { usePersistedState } from "../utils"
export const Counter = () => {
const [count, setCount] = usePersistedState("count", 0)
const increment = () => setCount(count + 1)
const decrement = () => setCount(count - 1)
return (
<div>
<h1>Counter</h1>
<p>Current count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
Our Counter component is now leveraging session storage to persist its state. If we increment our count a few times and then refresh the page, our count should persist.
Improvements with TypeScript.
TypeScript is a superset of JavaScript developed by Microsoft. One of the significant improvements TypeScript adds to our code is type enforcement. Our count state is implicitly typed by JavaScript as a number type. However, it will not throw an error or complain if we try to update our state with a string or boolean.
Let’s see how we can add that extra layer of code quality to our Counter and usePersistedState functions by converting them to TypeScript files.
import { useState, useEffect, Dispatch, SetStateAction } from "react"
function usePersistedState<T>(
key: string,
defaultValue: T
): [T, Dispatch<SetStateAction<T>>] {
const [state, setState] = useState<T>(
() => JSON.parse(sessionStorage.getItem(key)) || defaultValue
)
useEffect(() => {
sessionStorage.setItem(key, JSON.stringify(state))
}, [key, state])
return [state, setState]
}
export { usePersistedState }
First, let’s look at our custom hook.
The first thing you should notice is that it is a .ts file. Next, we’ve added two more imports from the ‘react’ package - Dispatch and SetStateAction. We make use of a fundamental feature of TypeScript - generics. A generic allows your function to be dynamically typed by the user. Here, we are using a generic type T. Whatever type is passed in by the user is then applied to the defaultValue. The type of key is set to string.
Our function also has a return type. It returns a tuple where the first value is of type T, and the second value is a SetStateAction Dispatch that accepts only an argument of type T.
Next, let’s see how our Counter component changes.
import React from "react"
import { usePersistedState } from "../utils"
export const Counter = () => {
const [count, setCount] = usePersistedState<number>("count", 0)
}
Our Counter file is now a .tsx file. When we use our custom hook, we can pass a type number to type our custom state hook explicitly.
Now we have some cool new features.
When we hover over the count variable and the setCount function, we can see their types.
// const count: number
const [count, setCount] = usePersistedState<number>("count", 0)
// const setCount: React.Dispatch<React.SetStateAction<number>>
const [count, setCount] = usePersistedState<number>("count", 0)
If we try to set our counter state with a variable that is not of type number, TypeScript will complain.
const setWrongType = () => setCount(true)
Error message: Argument of type ‘boolean’ is not assignable to parameter of type "SetStateAction<number>".
Now, our setCount function will ensure we always pass a parameter of type number to the setCount function.
Support Us
Support us by donating to our Patreon or buy me a coffee to keep us writing