What I Learned at Work this Week: Initialize with useState

I’ve been trying to build up the momentum to get started on a personal project recently, but it’s a challenge. There’s a lot that I’ve learned and forgotten, and the prospect of re-learning lessons while I struggle through a project on my own is…unappealing. Still, the benefits of practicing things like system architecture, database maintenance, and front end component design are huge for our professional growth. So this weekend I’m dipping my toes in the water a little bit by looking at something I don’t do often at work: React.

I started with a compilation video by Ben Awad, which reviews a series of React Hooks. useState was the first subject and, while most of it was familiar to me, he briefly mentioned an initializer which was new. Let’s talk about it.

My React is a little rusty, so it’s helpful for me to re-establish how we got into a situation where we were using this feature of the hook. We created a react app (npx create-react-app my-app-name) and cut out all the CSS and test files that come with the bootstrapping. That left me with index.js, which invokes a component called App.js. That’s where my code lives:

The useState Hook helps us maintain state in our app, meaning it helps persist and transform whatever data we need to render a UI. It’s particularly important with forms, which keep their own state by default. If we want to exert more control over our forms and more easily access their input values, React can use state to turn them into controlled components. That’s exactly what Ben does in his video, but he did it in a really cool way by building a custom hook around useState:

And here’s the application:

Besides learning a new functionality of useState, this tutorial also helped me build my very first custom Hook! So what’s going on in useForm?

We wrote this Hook because we want to be able to use a single function to manage the state of two different inputs. In this particular example, we don’t strictly need a custom Hook to make that work, but we’re also enjoying the benefit of abstracting our logic and separating our concerns. We should let our App component be responsible for rendering and leave the logic elsewhere.

When we invoke setState, we get an array in return. The first element of the array is the default value of our entity, which is the argument we pass (in this case, initialValues). The second element is a function that we can invoke to alter that value (known as the state dispatch function). This function replaces the initial value with a new one, which is a problem when dealing with our form.

Our form has two inputs: email and password. When representing it, we use an object with two properties: email and password. If we want to change the email, but leave the password the same, it’ll be difficult to use setState because updating the “email” input will trigger an event that contains no data about the password. So if the current state of our form values is:

{ email: '', password: '5F3ed4E'}

and we type in the email field, we’ll get an event.target that looks something like this:

{ value: 'a' }

We know that we want the state of our email field to be set to ‘a’, but we don’t want to reset the password field. Our custom Hook addresses this issue. First, it uses setState to establish a values object and a setValues function that will update the state. But when we return our array, the second element is a custom function:

e => {
setValues({
…values,
[e.target.name]: e.target.value
})
}

Just like any function created by setState, our custom function accepts an argument, e (short for event). It then uses setValues to update the state of the specific field being altered, while at the same time using the spread operator to leave the other field (or fields, if we expand our form in the future) unchanged.

We’re able to dynamically set the field in question by placing e.target.name in square brackets. Without those, we couldn’t use a variable to select or create a property name in an object.

In App.js, we invoke useForm and name the custom function handleChange. We invoke it onChange for both inputs, making our code easier to read, understand, and iterate on.

Later in the video, Ben gave an example of keeping a value from our state in localStorage so that it will persist through a page refresh. We can use the useEffect Hook to update localStorage each time our component renders:

We’ve given the Hook two arguments: a function and an array. The function dictates what logic should be executed when the values in our array, called the dependency array, change. I want to update localStorage each time our email value changes, so that goes in the dependency array.

This works to change the localStorage object on the first page load, but when I refresh the page, the email value in localStorage is set back to an empty string. This is because my useForm Hook is setting that value when the page loads, triggering useEffect to change localStorage back to the default value. To make this work, I have to change the default value of email to pull from localStorage:

Whatever value we use as a default state in a useState-type Hook is going to be referenced frequently, because the useState logic is run each time our component re-renders. But this article by Kent C. Dodds reminded me that this can be inefficient. After all, we only have to calculate the initial state when the page first loads. If that were an expensive calculation, we might see degraded performance as it’s run each time our state updates even though it’s no longer relevant because we’ve already altered it.

His alternative, as well as the alternative used in the video, was to turn the initial value into a function, otherwise known as an initializer function or lazy initialization:

Each time our state changes, useState still has to calculate the initial value passed as an argument. But this time, instead of querying localStorage, it simply defines a function that will only be called when needed on the initial rendering of our component, but not on re-renders. It doesn’t make much difference for my simple app, but it’s valuable when things start to scale.

I’ve written about Hooks before and I’ll admit that they’ve been a struggle, especially useEffect. When I first started writing my blog this weekend, I wondered if I was wasting time by re-explaining concepts I had already reviewed in previous articles. But something is different for me this time — concepts that I struggled with in the past make a lot more sense now. They’ve had time to sink in and I’ve seen more examples of them. I could have potentially learned more by studying a Hook I had never seen before, but I likely would have ended up with a tenuous grasp on two subjects rather than a firm grasp on a single one. This is also an important lesson — things are rarely easy at first, but that doesn’t mean we’ll be hopeless in our understanding. Our minds take time to absorb subjects and we may be surprised by how much more they make sense after we’ve allotted that time.

Solutions Engineer