The source code for this blog is available on GitHub.
Note
Top

React - useState Hook

Cover Image for React - useState Hook
Chen Han
Chen Han

React - useState Hook

This is a React component named UseState that uses the useState hook to manage a count state variable.

The component renders a div with a p element that displays the current value of count. It also renders four Button components that allow the user to increment or decrement the count.

The handleAdd, handleAnotherAdd, and handleThirdAdd functions are event handlers for the first three buttons, respectively. Each function updates the count state variable using setCount, which takes a new value for the state variable as an argument.

handleAdd and handleThirdAdd use a functional update, where the new value of count is calculated based on the previous value using a function passed to setCount. The difference between handleAdd and handleThirdAdd is that handleAdd passes the previous count value as an argument to the function, while handleThirdAdd creates a new newCount variable to hold the updated count value.

All three event handlers also display an alert message that shows the current value of count.

Finally, the component also imports an httpService module and uses it to make an HTTP GET request to https://jsonplaceholder.typicode.com/todos/ with the current count value as a parameter.

import { useState } from "react";
import Button from "@/components/Button";
import httpService from "@/services/httpService";

export default function UseState() {
  const [count, setCount] = useState(0);
  function handleAdd() {
    setCount((count) => count + 1);
    
    console.log(count)
    alert("what is count:" + count);
    httpService.get("https://jsonplaceholder.typicode.com/todos/" + count);
  }

  function handleAnotherAdd() {
    setCount(count + 1)
    alert("what is count:" + count);
    httpService.get("https://jsonplaceholder.typicode.com/todos/" + count);
  }

  function handleThirdAdd() {
    const newCount = count + 1;
    setCount(newCount);
    alert("what is count:" + count);
    httpService.get("https://jsonplaceholder.typicode.com/todos/" + count);
  }

  return (
    <div className="container mt-6">
      <p>Count: {count}</p>
      <Button
        small
        text="(count) => count + 1"
        type="primary"
        className="mr-2"
        onClick={handleAdd}
      />
      <Button
        small
        text="count + 1"
        type="primary"
        className="mr-2"
        onClick={handleAnotherAdd}
      />
      <Button
        small
        text="count + 1"
        type="primary"
        className="mr-2"
        onClick={handleThirdAdd}
      />
      <Button
        small
        text="count - 1"
        type="primary"
        onClick={() => setCount(count - 1)}
      />
    </div>
  );
}

Simple Update and Functional Update - Difference between handleAdd and handleAnotherAdd

Both handleAdd and handleAnotherAdd are event handlers for buttons in a React component that uses the useState hook to manage state. The main difference between handleAdd and handleAnotherAdd is how they update the state variable and display the current count value.

In handleAdd, the setCount function is called with a functional update that takes the previous count value as an argument and returns the new count value by adding 1 to it. This approach ensures that the count state is updated correctly even if multiple updates are triggered in rapid succession. After calling setCount, handleAdd displays an alert dialog that shows the previous count value.

In handleAnotherAdd, the setCount function is called with a simple update that adds 1 to the previous count value. Unlike handleAdd, this approach does not guarantee that the count state will be updated correctly in all cases. After calling setCount, handleAnotherAdd displays an alert dialog that shows the current count value, which is the updated value after calling setCount.

So, in summary, handleAdd updates the state using a functional update and shows the previous count value in the alert dialog, while handleAnotherAdd updates the state using a simple update and shows the current count value in the alert dialog.

In React's useState hook, there are two ways to update the state of a component:

  1. Simple update: In a simple update, the new state value is provided directly to the setState function. For example, if you have a state variable called count, you can update it using a simple update like this:

    setCount(42);
    

    In this case, the count state is immediately set to the new value of 42.

  2. Functional update: In a functional update, the new state value is computed based on the previous state value. This is useful when you need to update the state based on the previous value in a way that is independent of the timing of state updates. For example, if you have a state variable called count, you can update it using a functional update like this:

    setCount((prevCount) => prevCount + 1);
    

    In this case, the setCount function receives a function as an argument, which takes the previous state value prevCount as an argument and returns the new state value by adding 1 to it. This approach ensures that the new state value is based on the most recent previous state value, even if multiple state updates occur in rapid succession.

Functional updates are also useful for avoiding issues with stale state values. For example, if you have a state variable that depends on another state variable, you can use a functional update to ensure that you always get the latest value of the other variable:

setCount((prevCount) => prevCount + 1);
setTotalCount((prevTotalCount) => prevTotalCount + prevCount);

In this case, setTotalCount uses a functional update that takes the previous value of prevCount as an argument and adds it to the previous value of prevTotalCount, ensuring that the state is updated correctly even if multiple state updates occur in rapid succession.

Common Pitfalls When Using useState in React

The following examples are related to the useState hook in React. The first example demonstrates how the value of a state variable may be different at different points in time, while the second example shows how to update the state correctly using the useState hook and the useEffect hook in React.

While the examples may be different in terms of their specific use cases and implementation, they both involve the use of the useState hook in React.

useState is used to manage state within a component, and it provides a way to update the state asynchronously. When you call the useState hook, it returns an array with two values: the current state value, and a function that you can use to update the state value. When you call the state update function, React schedules a re-render of the component with the updated state value. This update may not happen immediately, but instead may happen asynchronously, in a batched update.

Real example - Difference between "count" on screen and on alert

import { useState } from "react";
import Button from "@/components/Button";
import httpService from "@/services/httpService";

export default function UseState() {
  const [count, setCount] = useState(0);
  function handleAdd() {
    setCount((count) => count + 1);
    alert("what is count:" + count);
    httpService.get("https://jsonplaceholder.typicode.com/todos/" + count);
  }

  function handleAnotherAdd() {
    setCount(count + 1)
    alert("what is count:" + count);
    httpService.get("https://jsonplaceholder.typicode.com/todos/" + count);
  }

  function handleThirdAdd() {
    const newCount = count + 1;
    setCount(newCount);
    alert("what is count:" + count);
    httpService.get("https://jsonplaceholder.typicode.com/todos/" + count);
  }

  return (
    <div className="container mt-6">
      <p>Count: {count}</p>
      <Button
        small
        text="(count) => count + 1"
        type="primary"
        className="mr-2"
        onClick={handleAdd}
      />
      <Button
        small
        text="count + 1"
        type="primary"
        className="mr-2"
        onClick={handleAnotherAdd}
      />
      <Button
        small
        text="count + 1"
        type="primary"
        className="mr-2"
        onClick={handleThirdAdd}
      />
      <Button
        small
        text="count - 1"
        type="primary"
        onClick={() => setCount(count - 1)}
      />
    </div>
  );
}

What happen between the count on the alert and the count variable on Count: {count}?

In React, when you call the useState hook, it returns a pair of values: the current state value and a function that allows you to update the state. In this component, the count state variable is being updated by the setCount function, which is called in response to the handleAdd, handleAnotherAdd, and handleThirdAdd functions.

When the component renders, the current value of count is used to render the Count component that displays the count value on the screen. So, when you see the Count component being displayed with a value of, say, 5, it means that the count state variable has been updated to a value of 5.

On the other hand, when you see an alert pop up with the value of count, it is showing the value of count at the time the alert function is called. It is important to note that the alert function does not block the execution of the rest of the code, so the value of count may have changed by the time you dismiss the alert. The value of count on the Count component and the value of count in the alert may not be the same, since they may have been updated at different times.

So probably count will show 4 at alert section and show 5 on the screen

Yes, that's correct! If the current value of count is 4 and you click the button that calls handleAdd, the setCount function will be called with a new value of count + 1, which is 5. However, at the time the alert function is called, the value of count is still 4. So the alert will show what is count: 4. After the alert is dismissed, the httpService.get function is called with a URL that includes the current value of count, which is now 5. Finally, the Count component will re-render with the updated value of count, which is now 5.

Real example - shipping address2 will accidentally prefill the billing address 2

Daniel: When I fill billing address, apt/suite for billing address has already filled. I don't think that should be happening, usually if its a different billing address. It also mean that it will probably be a different address altogher, no way address2 will be the same or it will be very rare to have two different addresses with same suite

Me: There appears to be a problem with the checkbox indicating that the billing address matches the shipping address.

Daniel pointed out that when he fills out the billing address, the apt/suite for billing address is automatically prefilled. This shouldn't happen because the billing address is often different from the shipping address, and it's unlikely for two different addresses to have the same suite.

I suspect that the issue is with the checkbox that indicates whether the billing address matches the shipping address. The code snippet below shows that when the checkbox is toggled, the status of sameAddress is not being updated correctly. The console logs show that the status is always false, even after toggling.

const handleClickSameAddress = () => {
  console.log("The status of billing address is the same as the shipping address (before toggle):", sameAddress);
  setSameAddress((prev) => !prev);
  console.log("The status of billing address is the same as the shipping address (after toggle):", sameAddress);
};

The result here will be as the following

The status of billing address is the same as the shipping address (before toggle): false
The status of billing address is the same as the shipping address (after toggle): false

The issue occurs when toggling the status of the billing address checkbox. The current implementation of handleClickSameAddress sets the state of sameAddress to the opposite value of the previous state, and then checks the value of sameAddress to decide whether to set the billing address the same as the shipping address or reset the state.

However, this implementation can result in unexpected behavior because the check for sameAddress happens before the state has been updated. Therefore, if the checkbox is unchecked (i.e., sameAddress is false), the function will incorrectly set the billing address the same as the shipping address, and vice versa.

const handleClickSameAddress = () => {
  setSameAddress((prev) => !prev);
  if(sameAddress) {
    // set the billing address the same as shipping address 
  } else {
    // reset the state
  }
};

We can use useEffect to correct the wrong behaviour mentioned above

In React, state updates are asynchronous, which means that the updated state value may not be immediately available after it has been set. In the provided code, setSameAddress is called with the new value for sameAddress, but it may not update the state immediately.

Therefore, when you log sameAddress immediately after calling setSameAddress, you may still see the previous value of sameAddress. This is because React may batch state updates for performance reasons, and only apply them when it's necessary.

To ensure that you see the updated value of sameAddress, you can use the useEffect hook and add sameAddress as a dependency. This will ensure that the console.log statement runs after the state has been updated:

useEffect(() => {
  console.log("The status of billing address is the same as the shipping address (after toggle):", sameAddress);
}, [sameAddress]);

This way, the console log statement will be executed every time sameAddress changes, and you will see the updated value.

Also, we can use another way to fix that issue. The function should use the updated value of sameAddress returned by the setSameAddress function.

const handleClickSameAddress = () => {
  setSameAddress((prev) => {
    if (!prev) {
      // set the billing address the same as the shipping address 
    } else {
      // reset the state
    }
    return !prev;
  });
};

Why useState

const Counter = () => {
  let counter = 0;

  const onClick = () => {
    counter = counter + 1;
    console.log(counter);
  };

  return (
    <>
      <button onClick={onClick}>click to update counter</button>
      Counter value: {counter}
    </>
  );
};

This is not going to work of course. In our console.log we’ll see the updated counter value, but the value rendered on the screen is not going to change - variables don’t cause re-renders, so our render output will never be updated.

useState, on the other hand, will work as expected: that’s exactly what state is for

usePrevious

This component demonstrates how custom hooks can be used to extend the functionality of React components and make them more reusable. The usePrevious hook in particular can be useful for tracking changes in state variables and performing logic based on those changes.

import { useState, useEffect, useRef } from "react";
import Button from "@/components/Button";

const usePrevious = (value: any) => {
  // create a new reference
  const ref = useRef();

  // store current value in ref
  useEffect(() => {
    ref.current = value;
  }, [value]); // only re-run if value changes

  // return previous value (happens before update in useEffect above)
  return ref.current;
};

const UsePrevious = () => {
  const [count, setCount] = useState(1);

  // get the previous value passed into the hook on the last render
  const prevCount = usePrevious(count);

  // show both current and previous value
  return (
    <div className="container mt-6">
      <div>
        Now: {count}, before: {prevCount}
      </div>
      <Button
        small
        text="add"
        type="primary"
        className="mr-2"
        onClick={() => setCount(count + 1)}
      />
    </div>
  );
};

export default UsePrevious;

The code provided is a React component that demonstrates the use of a custom hook called usePrevious. This hook allows the component to keep track of the previous value of a state variable, so that it can be used for comparison or other purposes.

In the component, the useState hook is used to create a state variable count with an initial value of 1. The usePrevious hook is then called to create a reference to the previous value of count.

The usePrevious hook works by using the useEffect hook to store the current value of count in a ref object. This useEffect hook is only re-run when count changes, so the ref object will always contain the previous value of count.

Finally, the component renders a div that displays both the current value of count and its previous value. Whenever the "add" button is clicked, the setCount function is called to update the value of count, and the usePrevious hook is used to retrieve the previous value of count and display it on the screen.

Why it can get previous data from by assigning the count value to ref.current. That's puzzling

When the component mounts, useEffect runs and assigns the initial value of count to the ref. As the component re-renders, the useEffect function runs again, but this time with the new value of count. Since the ref is being updated with the new value of count, prevCount always contains the previous value of count before it was updated.

In React, useRef creates a mutable object that can store a value and persists across renders. The useEffect hook runs after every render of the component and allows for side effects, like updating a ref. Since useEffect runs after the render, prevCount will contain the previous value of count from the previous render. This is how prevCount is able to hold the previous value of count and is returned by usePrevious hook.

The answer from stack overflow:

While this technically works, it's a confusing way of doing it and may lead to bugs as you add more stuff. The reason it works is because useEffect runs after state changes, and changing ref values don't cause a re-render. The way the code you posted works is as follows:

  1. Initially, both would be empty
  2. User types something, triggering a state change via setValue
  3. This triggers a re-render, so {value} is the new value, but since the ref hasn't been updated yet, {prevValue.current} still renders as the old value
  4. Next, after the render, the effect runs, since it has value as a dependency. So this effect updates the ref to contain the CURRENT state value
  5. However, since changing a ref value doesn't trigger a re-render, the new value isn't reflected in what's rendered

So once those steps above finish, then yes technically the state value and the ref are the same value. However, since the ref change didn't trigger a re-render, it still shows the old ref value in what's rendered.

This is obviously not great, cos if something else triggers a re-render, like say you had another input with a connected state value, then yes the {prevValue.current} would then re-render as the current {value} and then technically be wrong cos it would show the current value, not previous value.

I really don't know how ref.current = value works

useRef returns an object with a single property called current, which is initialized to the initialValue passed to useRef().

When useRef is called inside a component, it returns the same ref object on every render. By changing the current property of this ref object, you can store any mutable value that you want to preserve between re-renders.

In the code you provided, ref.current is assigned the value of value passed to usePrevious hook. This value is then persisted across re-renders, allowing you to access it in subsequent renders. This is why you are able to retrieve the previous value of the count state using the prevCount variable.

How do it persisted across re-renders

The value of ref.current is persisted across re-renders because the useEffect hook is used with a dependency array that includes the value argument. When the value changes, the effect callback is triggered, which updates the value of ref.current to the new value.

Since the useEffect hook only runs after the render phase, the previous value is always available on the next render. This allows the hook to store and retrieve the previous value of the value argument every time the component is re-rendered, even though the value variable is reset to a new value on every render.

lazy initializer

In React, a lazy initializer is a function that is used to initialize state lazily, meaning that the state is not immediately set when the component is mounted. Instead, the state is set when it is first accessed.

Lazy initialization is commonly used with the useState hook, which is a way to add state to a functional component in React. When using the useState hook, you can provide a lazy initializer as the initial value for the state.

Here is an example of using a lazy initializer with the useState hook:

import { useState } from "react";

function MyComponent() {
  const [count, setCount] = useState(() => {
    console.log("Initializing count...");
    return 0;
  });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

In this example, the lazy initializer is the arrow function () => { console.log("Initializing count..."); return 0; }. This function is only called the first time the count state is accessed, which is when the component is first rendered. The function sets the initial value of count to 0 and logs a message to the console.

Using a lazy initializer can be useful for expensive calculations or operations that are only needed in certain cases. By using a lazy initializer, you can delay the execution of these operations until they are actually needed, which can help improve performance.

Reference

  • The most common react state pitfall I see beginners fall into - Link
  • useState to implement usePrevious - Link
  • useRef and usePrevious - Link
© 2024 WOOTHINK. All Rights Reserved.
Site MapTerms and ConditionsPrivacy PolicyCookie Policy