5 Concepts you must know as a React Developer
Introduction
React is an easy-to-use and learn front-end library. But there are some concepts that a developer should know to write performant and efficient code.
I have compiled some gotchas and concepts of how state and effects work in React. I am sure you will learn something new today!
Deriving the State
If there is a state variable that depends on another state, you might think of using the general approach of using an useEffect
hook and update the depending state variable based on that.
Let’s understand it with an example. Suppose you are having a select
element containing the user ids of different users. You are tracking the selected user id using a userId
state variable.
import { useState } from "react"
const users = [
{ id: "1", name: "User One" },
{ id: "2", name: "User Two" },
{ id: "3", name: "User Three" },
]
function Users () {
const [userId, setUserId] = useState("1")
return(
<select value={userId} onChange={e => setUserId(e.target.value)}>
<option value="1">User One</option>
<option value="2">User Two</option>
<option value="3">User Three</option>
</select>
);
}
You also want to show the selected user on the screen. So you create another state variable called selectedUser
and set it when the userId
changes with the help of the useEffect
hook.
import { useState, useEffect } from "react"
function Users () {
const [userId, setUserId] = useState("")
const [selectedUser, setSelectedUser] = useState(undefined)
useEffect(() => {
setSelectedUser(users.find(u => u.id === userId))
}, [userId])
return(
<>
<select value={userId} onChange={e => setUserId(e.target.value)}>
<option>Select a user</option>
<option value="1">User One</option>
<option value="2">User Two</option>
<option value="3">User Three</option>
</select>
{selectedUser && <p>The selected user is: {selectedUser.name}</p>}
</>
);
}
This works, but this isn’t the best way you could have displayed the selected user.
The drawback of the above method is it first renders when userId
changes, and then the effect is triggered after the end of the render because userId
is passed in its dependency array. The useEffect
sets selectedUser
by finding it from the array.
This means that the component renders twice just for updating the selectedUser
, first time, when the userId
changes, and a second time, when the useEffect
updates the selectedUser
. Let’s see a better approach.
function Users () {
const [userId, setUserId] = useState("")
const selectedUser = users.find(u => u.id === userId)
return(
<>
<select value={userId} onChange={e => setUserId(e.target.value)}>
<option>Select a user</option>
<option value="1">User One</option>
<option value="2">User Two</option>
<option value="3">User Three</option>
</select>
{selectedUser && <p>The selected user is: {selectedUser.name}</p>}
</>
);
}
Why does this work? User selects userId
-> New render is triggered -> The code runs again and selecterUser
is set while rendering. As you can observe, this renders only one time instead of two.
You could have also done the below if you are using the selectedUser
at only one place and don’t need a variable to store that.
<p>The selected user is: {users.find(u => u.id === userId)?.name || ""}</p>
Set State inside Event Handlers
When there is a state update inside event handlers or effects, React re-renders the component. But this is not immediate. The re-render takes place only after the closing brace of the handler function.
function App () {
const [name, setName] = useState("")
const [age, setAge] = useState("")
const handleChange = (newName, newAge) => {
setName(newName) // batches name to be updated
setAge(newAge) // batches age to be updated
// other code...
console.log(name, age) // still the old name and age
} // at this point, finally updates the state that was batched above and re-renders
}
Also, note that it will also work the same for the code inside useEffect
.
Cleanup Functions
The useEffect
hook allows you to return a function that runs before running the useEffect
of the next render and also before unmounting the component. This function can be used as a way for unsubscribing the events the component no longer needs.
useEffect(() => {
button.addEventListener("click", listener)
return () => {
button.removeEventListener("click", listener)
}
}, [])
The returned function from the useEffect
is called a clean-up function. In the example above, we are removing the attached event listener before the next render or before the component is unmounted.
This is also useful for discarding the results from API calls. Suppose you make an API call by using the userId
selected by the user. So in this case, you should be able to discard the result of the previous userId
.
This is important because if the previous API call took longer than the current one, the previous API call result would get set as the current state which is not the expected outcome. We can store a flag inside the effect to counter this.
useEffect(() => {
let ignoreThisReq = false
fetch(`/api/users/userId`).then((res) => {
// if this is true, this effect already belongs to a previous render
// so ignore the received data
if(!ignoreThisReq) {
setUser(res.data)
}
})
return () => {
// clean up function is called, so discard the response from API
ignoreThisReq = true
}
}, [userId])
To play around with this, you can use the example below.
function User () {
const [clicks, setClicks] = useState(0)
const [clickedText, setClickedText] = useState("")
useEffect(() => {
setTimeout(() => {
setClickedText(`Clicked ${clicks} times`)
}, Math.random() * 5 * 1000)
}, [clicks])
return (
<>
<p>{clickedText}</p>
<button onClick={() => setClicks(c => c+1)}>Click Me</button>
</>
)
}
Try to click the button fast a few times, you will notice that the clickedText
will have a random value and not the latest clicks
value. Update the useEffect
to discard the old values.
useEffect(() => {
let ignorePrev = false
setTimeout(() => {
if(!ignorePrev) {
setClickedText(`Clicked ${clicks} times`)
}
}, Math.random() * 5 * 1000)
return () => {
ignorePrev = true
}
}, [clicks])
Updating the State when a Prop changes
There are times when you want some state variables to update or reset when a prop changes. Generally, this is done using the useEffect
hook like below.
useEffect(() => {
setSomeState(defaultValue)
}, [someProp])
This approach is easy and it works, but leads to the component and its children being rendered twice. Instead, we can store the previous value of the prop variable and then compare the current and previous prop.
The advantage of using this approach over useEffect
is this approach checks the prop value while rendering and triggers a re-render instantly when the state is updated and the children won't be rendered twice.
Let’s see the useEffect
approach with an example
import React, { useState, useEffect } from 'react'
function App () {
const [userId, setUserId] = useState("1")
return (
<>
<select value={userId} onChange={e => setUserId(e.target.value)}>
<option value="1">User 1</option>
<option value="2">User 2</option>
<option value="3">User 3</option>
</select>
<User userId={userId} /> // user component
</>
)
}
function User ({ userId }) {
const [clicks, setClicks] = useState(0) // record no. of clicks for current user
useEffect(() => {
setClicks(0) // reset no. of clicks when user changes
}, [userId])
console.log("User rendered")
return (
<>
<p>No. of clicks {clicks}</p>
<button onClick={() => setClicks(c => c+1)}>Click Me</button>
<UserChild />
</>
)
}
function UserChild () {
console.log("Child rendered")
return <p>User's child</p>
}
The above code will print User rendered, Child rendered, User rendered, Child rendered
when you switch a user. Notice how UserChild
is rendered twice. Let’s see the other approach, update the User
component like below.
function User ({ userId }) {
const [clicks, setClicks] = useState(0) // record no. of clicks for current user
const [prevUserId, setPrevUserId] = useState(user) // record previous prop
if(userId !== prevUserId) { // this means userId prop changed
setPrevUserId(userId)
setClicks(0) // reset no. of clicks when user changes
}) // component triggers a re-render at this point
console.log("User rendered")
return (
<>
<p>No. of clicks {clicks}</p>
<button onClick={() => setClicks(c => c+1)}>Click Me</button>
<UserChild />
</>
)
}
The above example will print User rendered, User rendered, Child rendered
when you switch the user.
The UserChild
component is only rendered once because a re-render was already triggered while rendering the User
component, so it skips rendering its children (UserChild
) and renders the children on the re-render.
Note that the User
component will still be rendered twice (not the children).
State preservation in the same position
The state of a component is preserved if it’s rendered at the same position in the UI tree. This means that if you are conditionally rendering the same component with different props, the state will not change.
Let’s have a look at an example.
function App () {
const [userId, setUserId] = useState("1")
return (
<>
<select value={userId} onChange={e => setUserId(e.target.value)}>
<option value="1">User 1</option>
<option value="2">User 2</option>
</select>
{
userId === "1" ?
<User userId={userId} username="User 1" /> :
<User userId={userId} username="User 2" />
}
</>
)
}
function User ({ userId, username }) {
const [clicks, setClicks] = useState(0)
return (
<>
<p>No. of clicks for {username} : {clicks}</p>
<button onClick={() => setClicks(c => c+1)}>Click Me</button>
</>
)
}
The above example stores the number of clicks for each user. What do you think happens when you switch the user? The click count still remains the same!
Although we are rendering two different components for different users, they are rendered at the same position, and React thinks it is the same component and preserves the state.
Use key
for letting React know that these are different components. This tells React that this is a component with the key
unique id and treats it as a separate component.
{
userId === "1" ?
<User key={userId} userId={userId} username="User 1" /> :
<User key={userId} userId={userId} username="User 2" />
}
Or, you can use the &&
operator to render the components at separate positions.
{userId === "1" && <User userId={userId} username="User 1" />}
{userId === "2" && <User userId={userId} username="User 2" />}
Conclusion
The above concepts might not seem significant for small web apps, but the performance will surely improve for components with deeply nested children and larger projects.
Thanks for reading, see you at the next one!