Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useRef: Counting renders doesn´t work as expected in React neither 18.0.1 nor 18.0.2 #27880

Closed
jhojan789 opened this issue Jan 5, 2024 · 2 comments
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@jhojan789
Copy link

jhojan789 commented Jan 5, 2024

I am trying to count each instance I type a letter into an input. In React 17.0.2 works fine: 1,2,3,4,... . But in React 18.0.1 and 18.0.2 the counting starts from 2 jumping over the number 1: 2,3,4,...

React version: 18.0.1 and 18.0.2

Steps To Reproduce

  1. Replace the App.js code with the code given below in a new React 18.0.1 and 18.0.2 project.
  2. npm start and type letters into the input field. You will see the counting starts from 2 and not from 1 as it should be.

Code:

import { useEffect, useRef, useState } from "react";

function App() {
  const [inputValue, setInputValue] = useState("");

  const ref = useRef(0);

  useEffect(() => {
    ref.current = ref.current + 1;
  });

  return (
    <>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <p>{ref.current}</p>
    </>
  );
}

export default App;

The current behavior

2,3,4...

The expected behavior

1,2,3,4...

@jhojan789 jhojan789 added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Jan 5, 2024
@AliHaiderPgm
Copy link

The issue you're experiencing is due to the way React handles updates and re-renders. When your component first renders, ref.current is initialized to 0. However, in the same render cycle, you're incrementing ref.current inside a useEffect hook without any dependencies. This means that the effect will run after every render, including the initial one. So, on the first render, ref.current is incremented from 0 to 1, then on the second render it's incremented again from 1 to 2, and so on. That's why you're seeing 2, 3, 4, etc., instead of 1, 2, 3, etc.

To fix this, you need to ensure that ref.current is only incremented once per render cycle. One way to do this is to use a separate state variable to track whether it's the first render or not. Here's how you can modify your code:

First Method is using useState hook:

//previous code
const [isFirstRender, setIsFirstRender] = useState(true);
const ref = useRef(0);
 useEffect(() => {
   if (isFirstRender) {
   setIsFirstRender(false);
   } else {
     ref.current = ref.current + 1;
   }
 });
//previous code

Second method is using useRef hook:

//previous code
const log = useRef(true)
const ref = useRef(0);
useEffect(() => {
    if (log.current) {
      log.current = false
    } else {
      ref.current = ref.current + 1;
    }
  });
//previous code

@rickhanlonii
Copy link
Member

@AliHaiderPgm thats a good thought, but the effect doesn't run until after render so it will show 0,1,2,3,4 as expected as written outside of strict mode. Your fix does solve the issue, because it skips the count for the first strict mode effect, but if this was run in prod it would miss the first render, since strict mode doesn't run in prod.

The issue is that React 18 introduced a new feature for StrictMode that immediately unmounts and remounts effects to ensure they're properly cleaned up, to catch bugs like this. That's why it worked before 18 and doesn't work as expected.

Check out this sandbox, and notice that is works if you remove from index.js: https://codesandbox.io/p/sandbox/blissful-darkness-xyz6m6?file=%2Fsrc%2Findex.js%3A10%2C18

The reason this is a bug is because when the component unmounts, you're not resetting the ref. That means when you use certain features, the ref won't be reset when you expect it to. For example, when Fast Refresh updates the component, you would expect the render count to reset to 0, but in your version the render count gets "stuck" at the current value and you need to hard refresh the page to reset it to 0.

In future versions we plan to reset refs for you in strict mode to align with the production behavior (#25049), but hopefully this shows the value of the new strict mode behavior.

Until that lands, to fix this, you can reset the ref when the component unmounts. You need two effects to do this because the effect that updates the count will destroy on every render, but you only want to reset on unmount:

const ref = useRef(0);

useEffect(() => {
  ref.current = ref.current + 1;
});

useEffect(() => {
  return () => {
    ref.current = 0;
  };
}, []);

Note: we also have a planned feature to support doing this in a single effect by allowing you to specify different functions that run for create vs update in the same effect: #25744

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests

3 participants