GithubHelp home page GithubHelp logo

Comments (6)

RajiDevMind avatar RajiDevMind commented on April 28, 2024 1

I think the issue came from the null checks.

export function useCache() {
const params = useCacheParams();

const cache = useRef<Cache | null>(null);
if (cache.current == null) {
cache.current = new Cache(params);
}

useEffect(() => {
return () => {
if (cache.current) {
cache.current.close();
}
};
}, []);

return cache.current!;
}

from react.

rickhanlonii avatar rickhanlonii commented on April 28, 2024 1

This is not a bug in StrictMode but a bug in the code, which StrictMode catches.

@glyph-cat in your example, if you turn off StrictMode, and run the app it will work initially, but if you try to change the file so that it Fast Refreshes, you'll get the same error: https://codesandbox.io/p/sandbox/determined-austin-8kqjnk

This is because there are features in React that allow components to unmount effects but preserve the state of the component, then when the component mounts again, only the effects need to be recreated. This is why Effects need to be symmetrical, so that things that are created are destroyed, and things that are destroyed are created.

To fix this example, you must set the ref to null in destroy, and re-create the utility in create:

if (utilityRef.current === null) {
  utilityRef.current = new SomeUtility();
}

useEffect(() => {
  if (utilityRef.current === null) {
    utilityRef.current = new SomeUtility();
  }

  return () => {
    utilityRef.current.dispose();
    utilityRef.current = null;
  };
}, []);

Notice that with this change, the component doesn't error during either Fast Refresh, or in StrictMode: https://codesandbox.io/p/sandbox/peaceful-moon-vfdf6c

Note, I also needed to update useCustomHook to accept the ref instead of ref.current.

Here's some docs for more info:

from react.

glyph-cat avatar glyph-cat commented on April 28, 2024

Same issue here, so let me provide some additional information:

Cache will be created twice but closed only once.

It was the instance of cache created by the second invocation (from StrictMode) that was being closed, so the first one never got the chance to cleanup.

Allow me to explain with a slightly different example (written in TSX):

class SomeUtility {

  private static id = 0
  private readonly id: number = SomeUtility.id++
  private isDisposed = false

  constructor() {
    console.log(`TestClass created [id: ${this.id}]`)
  }

  dispose = () => {
    if (this.isDisposed) {
      throw new Error(`Attempted to dispose but SomeUtility [id: ${this.id}] has already been disposed`)
    }
    this.isDisposed = true
    console.log(`TestClass disposed [id: ${this.id}]`)
  }

  greet = () => {
    if (this.isDisposed) {
      throw new Error(`Attempted to greet but SomeUtility [id: ${this.id}] has already been disposed`)
    }
    console.log(`Hello world [id: ${this.id}]`)
  }

}

function useCustomHook(utility: SomeUtility): void {
  useEffect(() => {
    utility.greet()
  }, [utility])
}

let renderCount = 0
function TestComponent(): JSX.Element {
  const utilityRef = useRef<SomeUtility>(null)
  if (utilityRef.current === null) {
    utilityRef.current = new SomeUtility()
  }
  useEffect(() => {
    return () => { utilityRef.current.dispose() }
  }, [utilityRef])
  useCustomHook(utilityRef.current)
  console.log(`renderCount: ${++renderCount}`)
  // Console results:
  // ```
  //   TestClass created [id: 0]
  //   renderCount: 1
  //   TestClass created [id: 1]
  //   renderCount: 2
  //   Hello world [id: 1]
  //   TestClass disposed [id: 1]
  //   Uncaught Error: Attempted to greet but SomeUtility [id: 1] has already been disposed
  //   Uncaught Error: Attempted to dispose but SomeUtility [id: 1] has already been disposed
  // ```
  // ❌ TestClass [id: 0] was never disposed until the end
  // ❌ TestClass [id: 1] which should be used is being disposed instead
  return null
}

Still, this problem has been around for very long (at least one year before this thread has been opened) and I think it's a legitimate concern. This issue deserves more attention.

Edit: It seems like #26315 also mentions of this problem.

from react.

jacobtipp avatar jacobtipp commented on April 28, 2024

This is not a bug in StrictMode but a bug in the code, which StrictMode catches.

@glyph-cat in your example, if you turn off StrictMode, and run the app it will work initially, but if you try to change the file so that it Fast Refreshes, you'll get the same error: https://codesandbox.io/p/sandbox/determined-austin-8kqjnk

This is because there are features in React that allow components to unmount effects but preserve the state of the component, then when the component mounts again, only the effects need to be recreated. This is why Effects need to be symmetrical, so that things that are created are destroyed, and things that are destroyed are created.

To fix this example, you must set the ref to null in destroy, and re-create the utility in create:

if (utilityRef.current === null) {
  utilityRef.current = new SomeUtility();
}

useEffect(() => {
  if (utilityRef.current === null) {
    utilityRef.current = new SomeUtility();
  }

  return () => {
    utilityRef.current.dispose();
    utilityRef.current = null;
  };
}, []);

Notice that with this change, the component doesn't error during either Fast Refresh, or in StrictMode: https://codesandbox.io/p/sandbox/peaceful-moon-vfdf6c

Note, I also needed to update useCustomHook to accept the ref instead of ref.current.

Here's some docs for more info:

* https://react.dev/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development

* https://react.dev/reference/react/StrictMode#fixing-bugs-found-by-re-running-effects-in-development

@rickhanlonii The first render still creates an object that will never be disposed, because the first useEffect call happens after the rerender. Is there any way to dispose the first object?

In most cases this shouldn't be a problem in development, but I can see perhaps some objects may have active listeners that will never clean up.

from react.

rickhanlonii avatar rickhanlonii commented on April 28, 2024

@jacobtipp yeah StrictMode is catching a separate bug, which is that your component isn't pure. There's a side effect of mutating the id variable in render.

The solution is to not mutate in render. If this is an expensive resource, you also shouldn't be creating it in render because React may render multiple times before a component mounts (or may never mount at all) and you'll leak instances.

For example, say a child in this tree suspends, and the Suspense boundary is above this component. Then this component may render multiple times before the effect fires, creating multiple SomeUtility instances that are not cleaned up. If the user navigates away while you're suspended, then the component never mounted, so the effect cleanup won't fire and the resource won't be destroyed. So this should really be done in an effect, when you know the component has mounted.

from react.

jacobtipp avatar jacobtipp commented on April 28, 2024

@jacobtipp yeah StrictMode is catching a separate bug, which is that your component isn't pure. There's a side effect of mutating the id variable in render.

The solution is to not mutate in render. If this is an expensive resource, you also shouldn't be creating it in render because React may render multiple times before a component mounts (or may never mount at all) and you'll leak instances.

For example, say a child in this tree suspends, and the Suspense boundary is above this component. Then this component may render multiple times before the effect fires, creating multiple SomeUtility instances that are not cleaned up. If the user navigates away while you're suspended, then the component never mounted, so the effect cleanup won't fire and the resource won't be destroyed. So this should really be done in an effect, when you know the component has mounted.

A situation I'm dealing with is similar to this example but I need to create a resource and provide it to a subtree of components using React.Context (a Provider component that creates a disposable instance of a class and provides it to all children). If I create the resource in a useEffect I'm forced to either return an empty fragment until the resource is created, or return the context.Provider with null as its initial value. This is not ideal because consumers of this context are expecting a resource that they can use and subscribe to.

I was assuming I could create the resource in a ref like in this React docs example:

https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents

I'm guessing I don't have any options but to create the resource in useEffect since you say a component may not mount at all even after rendering multiple times.

from react.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.