1

I am implementing an attachment component and trying to be a good citizen by calling URL.revokeObjectURL() when my component is unmounted to cleanup the local blob resources.

Using a class component I would have just used ComponentWillUnmount and I assume that would have worked fine. Now that I am using a functional component I need to use a useEffect hook that has a cleanup function which invokes the URL.revokeObjectUrl method. This was my first approach

 useEffect(() => {
    return () => {
      attachments &&
        attachments.forEach((a) => {
          console.log('cleaning', a.uri);
          URL.revokeObjectURL(a.uri);
        });
    };
  });

but the attachment prop was undefined; I have read this is because its value is set from when the component is first rendered. So I tried to add the props to the array of dependencies

 useEffect(() => {
    return () => {
      attachments &&
        attachments.forEach((a) => {
          console.log('cleaning', a.uri);
          URL.revokeObjectURL(a.uri);
        });
    };
  }, [attachments]);

but then it runs the cleanup code whenever a new attachment is uploaded as the attachments object updates.

What is the right way to implement useEffect so that it only cleans up once when the component is unmounted? I found this SO article but wasn't too happy with the solution and thought there must be a better way

simondefreeze
  • 189
  • 16
  • Duplicate? [How to use componentWillMount() in React Hooks?](https://stackoverflow.com/questions/53464595/how-to-use-componentwillmount-in-react-hooks) – anthumchris Sep 23 '20 at 20:52
  • 1
    Hi @AnthumChris, that SO question is about the component mounting. I am looking for a solution around the component unmounting. The first answer to that SO question by Bhaskar Gyan Vardhan also tries to answer how to do unmounting using hooks however the example doesn't use any of the components props on unmounting. My problem that to access props in the useEffect hook I need to add a dependency, which then means the cleanup function runs whenever the dependency changes, I only want it to run once on unmounting. – simondefreeze Sep 23 '20 at 22:32
  • That answer should provide a few examples of `componentWillUnmount` as well. Are you not seeing them? – anthumchris Sep 24 '20 at 03:06
  • @simondefreeze Have you found a solution to this problem? I wanted to do exactly the same and also don't know how to properly call URL.revokeObjectURL(url) with URLs that I have in the component's state. – drazewski Nov 19 '20 at 12:02
  • Hi @drazewski unfortunately I haven't found a solution to this yet. I am not sure what I am missing here and I am surprised this is not reported by more people. It is a catch 22 situation - if you don't include the attachments in the dependencies array it will not have an up to date list of attachments when unmounting, but adding it to the dependency array means it will run the code on every update to the attachments array. – simondefreeze Nov 19 '20 at 21:59

3 Answers3

4

I encountered the same issue and figured out a way that works for me.

Instead of State, I use Ref to store the blobs. And I just do the cleanup for the Ref in my useEffect when the component unmount.

  React.useEffect(() => {
    return () => {
      // Cleanup blobs when component unmount
      ref.current.map((file) => URL.revokeObjectURL(file));
    };
  }, []);
Dharman
  • 30,962
  • 25
  • 85
  • 135
James Yang
  • 110
  • 7
  • 1
    Ahh very smart, I'll do that :) I still wish React had a more elegant way of doing this though and I am surprised more people don't have an issue with this. – simondefreeze Feb 26 '21 at 01:15
0

might this can help:

  const {getRootProps, getInputProps} = useDropzone({
    accept: 'image/*',
    onDrop: acceptedFiles => {
      setFiles(acceptedFiles.map(file => Object.assign(file, {
        preview: URL.createObjectURL(file)
      })));
    }
  });
  
  const thumbs = files.map(file => (
    <div style={thumb} key={file.name}>
      <div style={thumbInner}>
        <img
          src={file.preview}
          style={img}
        />
      </div>
    </div>
  ));

  useEffect(() => () => {
    // Make sure to revoke the data uris to avoid memory leaks
    files.forEach(file => URL.revokeObjectURL(file.preview));
  }, [files]);

from https://react-dropzone.js.org/#section-previews

L1l
  • 21
  • 2
  • Can you add some more info about how your answer answers the OP's question. And while links are great they can change so if you can add the relevant information from the link to your answer that would improve your answer as well. – Matthew Barlowe Nov 28 '21 at 07:41
  • If you have a new question, please ask it by clicking the [Ask Question](https://stackoverflow.com/questions/ask) button. Include a link to this question if it helps provide context. - [From Review](/review/late-answers/30441536) – Beso Nov 28 '21 at 08:17
0

If you want to stick to state instead of a reference, you can use the following solution:

useEffect(() => {
    // Only revoke data uris when the component mounts and unmounts
    return () => {
        attachments.forEach(() => {
          URL.revokeObjectURL(url);
        });
    };
  }, []);
MapMyMind
  • 113
  • 10