Slice of Dev Logo

5 Concepts you must know as a React Developer

Cover Image for 5 Concepts you must know as a React Developer
Author's Profile Pic
Rajkumar Gaur

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!


Cover Image for React Interview Experience

React Interview Experience

This blog is about my recent React interview experiences and some interesting questions that were asked. These questions might help you prepare for your next interview. Guess The Output | useState vs useReducer | useCallback and useMemo | Redux Vs Context API | Manage The Focus Of Elements Using React | Why is useRef used | Coding Problem.

Author's Profile Pic
Rajkumar Gaur
Cover Image for Streams in NodeJS

Streams in NodeJS

Node.js Streams are an essential feature of the platform that provide an efficient way to handle data flows. They allow for processing of large volumes of data in a memory-efficient and scalable way, and can be used for a variety of purposes such as reading from and writing to files, transforming data, and combining multiple streams into a single pipeline

Author's Profile Pic
Rajkumar Gaur