Refactoring your Component with React Hooks

And how to replace setState’s 2nd argument with a hook

Sean LaFlam
Geek Culture

--

React hooks allow you to mimic the behavior provided by class components

useState

The useState hook does exactly what it sounds like. It allows you use use state in your functional component. Let’s take a look how this is normally done in a class component and then make the changes to a functional component using the useState hook.

import React from 'react'class MyComponent extends React.Component
constructor(props) {
super(props);
this.state = {
name: "",
email: """,
student: false,
};
render() {
return (
<form>
{Some JSX Code Here}
</form>
)
}
export default MyComponent

Normally when creating state in a class component you use the constructor function and then declare a state object using this.state. Inside this object you then define some attributes which you’d like to store in state. In this case, we will have a firstName, lastName, and editingName property.

When you want to update state in React, you use the setState function provided by React. A more sophisticated component below shows how this works normally.

import React from 'react'class MyComponent extends React.Component
constructor(props) {
super(props);
this.state = {
name: "",
email: """,
studentStatus: false,
};
handleChange(event) {
event.preventDefault();
let name = event.target.name;
let value = event.target.value;

this.setState({name: value})
}

handleSubmit(event) {
event.preventDefault();
console.log(this.state);
}
render(){
return (
<form onSubmit={this.handleSubmit}>
<label> Name:
<input type="text" name="name" value={this.state.name} onChange={this.handleChange)} />
</label>
<label>Email:
<input type="text" name="email" value={this.state.email} onChange={this.handleChange}/>
</label><br />
<input className="btn" type="submit" value="Submit" />
</form>
)
}
export default MyComponent

The code above will update the corresponding value in state as you type using the onChange function. Then when submitted, it will log all the values of your updated state to the console. Simple enough.

Now let’s modify this component to be a functional component which uses the useState hook.

First, we’ll need to import useState from ‘react’

Then, replace the constructor with a destructuring of useState. The useState function returns an array of 2 elements. The first element is the current state, and the second, is a function that is used to update that 1st state value. You can call these whatever you want, but the general syntax you will see is [‘variableName’, ‘setVariableName’]. We’ll use that syntax here. You can also pass useState an argument, and whatever you pass it, the 1st value in the array will be initialized to equal that value.

For example, if you say ‘[count, setCount] = useState(10)’ you will get a state variable called count and it will be set equal to 10 to start. Then you can use setCount to update state like so: ‘setCount(count + 1)’ which will increment the count by 1 every time its called.

import React, {useState} from 'react'const MyComponent = () => {

[name, setName] = useState("")
[email, setEmail] = useState("")
[studentStatus, setStudentStatus] = useState(false)
fCh(event) { <-- shortened to fit(usually called changeHandler)
event.preventDefault();
let name = event.target.name;
let value = event.target.value;

if (name === 'name'){
setName(value)
} else {
setEmail(value)
}
}

handleSubmit(event) {
event.preventDefault();
console.log(this.state);
}
render(){
return (
<form onSubmit={handleSubmit}>
<label> Name:
<input type="text" name="name" value={name} onChange={fCh)}/>
</label>
<label>Email:
<input type="text" name="email" value={email} onChange={fCh}/>
</label><br />
<input className="btn" type="submit" value="Submit" />
</form>
)
}
export default MyComponent

You’ll notice that instead of storing all the values in your state inside one object, and then using setState to update, we now have 3 discrete variables that represent each value in state, and we have 3 separate functions to update each. There are ways to set it so that each value is still contained in one object, and only one function updates each, but for beginners I think it’s more clear to keep it separate the way I did above.

useEffect

The useEffect hook is how we mimic React class component’s lifecycle methods. Each use effect will, by default, run after the component is first rendered, and then again after each update or change to the component. (If this sounds very similar to componentDidMount and componentDidUpdate you’re right!)

We define the useEffect hook inside of the component so it has access to the state variables we need.

Let’s say we wanted to display some information that relies on state, but not until the component is rendered. In our old class component we would handle it like this:

import React from 'react'class MyComponent extends React.Component
constructor(props) {
super(props);
this.state = {
name: "",
email: """,
studentStatus: false,
};
handleChange(event) {
event.preventDefault();
let name = event.target.name;
let value = event.target.value;

this.setState({name: value})
}

handleSubmit(event) {
event.preventDefault();
console.log(this.state);
}
render(){
return (
<div>
<h1> {document.title} </h1>
<form onSubmit={this.handleSubmit}>
<label> Name:
<input type="text" name="name" value={this.state.name} onChange={this.handleChange)} />
</label>
<label>Email:
<input type="text" name="email" value={this.state.email} onChange={this.handleChange}/>
</label><br />
<input className="btn" type="submit" value="Submit" />
</form>
)
}
componentDidMount(){
document.title = `{name}'s Profile`
}
export default MyComponent

This is kind of a silly example because we could just use state, but the principal remains the same. When the component mounts, componentDidMount will set the document’s title using info grabbed from state.

If you want to mimic this behavior in your functional component, you will just use useEffect.

import React, {useState, useEffect} from 'react'const MyComponent = () => {

[name, setName] = useState("")
[email, setEmail] = useState("")
[studentStatus, setStudentStatus] = useState(false)
fCh(event) { <-- shortened to fit(usually called changeHandler)
event.preventDefault();
let name = event.target.name;
let value = event.target.value;
if (name === 'name'){
setName(value)
} else {
setEmail(value)
}
}

handleSubmit(event) {
event.preventDefault();
console.log(this.state);
}
useEffect(() => {
document.title = `{name}'s Profile`;
});
render(){
return (
<div>
<h1> {document.title} </h1>
<form onSubmit={handleSubmit}>
<label> Name:
<input type="text" name="name" value={name} onChange={fCh)}/>
</label>
<label>Email:
<input type="text" name="email" value={email} onChange={fCh}/>
</label><br />
<input className="btn" type="submit" value="Submit" />
</form>
)
}
export default MyComponent

The same thing will happen as above, where the document’s title will be updated but only once the component has rendered.

Recreating setState’s 2nd Argument with useEffect

Let’s say you had a component that has a function that focuses on a text box when you click it, and removes focus when you click outside.

useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);

return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
});

First, you’ll need to add a useEffect that adds these listeners to your page and calls the corresponding functions.

When using useEffect, if your function needs cleanup once it un-mounts (for example if you want to clear an interval for a timer, or remove an event listener) you can stick the cleanup function inside of the return value of the useEffect hook that adds the functionality you want to clean up. That’s what you see above. The code adds a mousedown event listener, and then removes it once the component un-mounts from the page.

In this component there is also

onTextChange = (event) => {    
if (event.target.value !== this.state.textContents + "\n") {
this.setState({ textContents: event.target.value }, () =>
this.textInput.current.focus()
);
}
};

You’ll see here the setState function has a 2nd argument of a callback function whose job is to focus on the ref containing the text input box. The reason it’s inside of the setState function is because this function will be called immediately after state is updated only through this specific function, and it will have access to the updated state value. Since setState is async, if this function was not inside the setState function, you cannot guarantee it will run with the most updated version of state.

Now this one is a little tricky to mimic, but we can accomplish this same effect by creating yet another useEffect, but having it only run when a specific state attribute is updated rather than on each update or on first render.

This Stack Overflow post does a really good job explaining how to accomplish this in more detail, but what you want to do is create a useEffect function and give it what is called a dependency array. The dependency array is a list of all of the variables for which you want updates to trigger this specific useEffect.

Here’s what that will look like in our new functional component:

const onTextChange = (event) => {
if (event.target.value !== textContents + "\n") {
setTextContents(event.target.value);
setEditingText(false)
}
};
useEffect(() => {
const textInput = textInput.current;
if (textInput && editingText) {
textInput.focus();
const endOfInput = textInput.textLength;
textInput.setSelectionRange(endOfInput, endOfInput);
}
}, [editingText]);

So the first onTextChange function uses the setTextContents function provided by useState, to update the textContents state variable and the setEditingText function to change the editingText variable to false. This signifies we are done editing the component.

Then, the useEffect with a dependency array of editingText, will notice that editingText has changed and it will run the code in order to either focus on the text box adding a cursor when the user is editing the text, or unfocus on the text box removing the cursor when the update is done.

The reason for the ‘const textInput = textInput.current’ is so that the code doesn’t run on initial render before the textInput ref is defined causing an error.

And there you have it. Your component should be fully refactored into a functional component using React hooks!

--

--