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

[React 19]useEffect cleaned the wrong state in StrictMode #31098

Open
dislido opened this issue Sep 30, 2024 · 7 comments
Open

[React 19]useEffect cleaned the wrong state in StrictMode #31098

dislido opened this issue Sep 30, 2024 · 7 comments
Labels

Comments

@dislido
Copy link

dislido commented Sep 30, 2024

Summary

https://codesandbox.io/p/sandbox/youthful-orla-lk7g89

import { useEffect, useState } from "react";

let stateId = 0;
export default function App() {
  const [state] = useState({
    stateId: console.log(++stateId, "created") || stateId,
    destroy() {
      console.log(`state ${this.stateId} destroyed`);
    },
  });
  useEffect(() => {
    return () => {
      state.destroy();
    };
  }, [state]);

  console.log("render state", state);
  return <div className="App">state: {state.stateId}</div>;
}

console output:

1 created
render state {stateId: 1, destroy: ƒ}
2 created
render state {stateId: 1, destroy: ƒ}
state 1 destroyed

Final render: state: 1, the destroyed state

[email protected] behavior

related issue: #29634

1 created
render state {stateId: 1, destroy: ƒ}
2 created
render state {stateId: 2, destroy: ƒ}
state 2 destroyed

Final render: state: 2, the destroyed state

@MohamedYassineBenomar
Copy link

This issue seems interesting! I would love to contribute or discuss further. 👍

@ry-krystal

This comment was marked as spam.

@akashvivek
Copy link

Is this issue still opened ?

@dislido
Copy link
Author

dislido commented Oct 8, 2024

Is this issue still opened ?

Yes, neither React 19.x nor 18.x has solved this issue

@kassens
Copy link
Member

kassens commented Oct 8, 2024

A reduced repro of this change is

import { useState } from "react";

let stateId = 0;
export default function App() {
  stateId++;
  const [state] = useState(stateId);
  return state;
}

What is happening here is that in React the App component is rendered twice. In 19 React preserves the state from the first render.

I think this is intentional by React preserving and re-using more state, but someone else might correct me.

@MeerArsalanAli27
Copy link

It seems there's an issue with how the useEffect cleanup function references the state in StrictMode. The current implementation logs the initial stateId during cleanup, which can lead to confusion since stateId increments with each render.

To fix this, consider using an updateState function that retains the destroy method while creating a new state with an incremented stateId. This ensures the cleanup function references the correct state:` import { useEffect, useState } from "react";

let stateId = 0;

export default function App() {
const [state, setState] = useState({
stateId: ++stateId,
destroy() {
console.log(state ${this.stateId} destroyed);
},
});

useEffect(() => {
return () => {
state.destroy();
};
}, [state]);

const updateState = () => {
setState({
stateId: ++stateId,
destroy: state.destroy, // Retain the destroy function from the previous state
});
};

console.log("render state", state);

return (


state: {state.stateId}

Update State

);
}
`

@dislido
Copy link
Author

dislido commented Oct 10, 2024

A more practical example

//In this example, it is a websocket connection, which can also be any object that is not suitable for creating in each rendering and should be destroyed when not use
class Connection {
  constructor() {
    // create ws connection
  }
  destroy() {
    // close ws connection
  }
}

function initState() {
  return new Connection();
}
export default function App() {
  const [connection, setConnection] = useState(initState);
  useEffect(() => {
    return () => {
      connection.destroy();
    };
  }, [state]);

  const reconnect = () => setConnection(initState())
  return null;
}

Of course, we can easily solve this problem through the following way:

// init connection in useEffect
export default function App() {
  const [connection, setConnection] = useState();
  useEffect(() => {
    const newConnection = initState();
    setConnection(newConnection)
    return () => {
      newConnection.destroy();
    };
  }, [state]);
  // But the type of connection become `Connection | undefined`, 
  // This will add some additional check logic
  useMemo(() => {
    if (!connection) return null; // <-
    return connection;
  }, []);

  return null;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants