Search code examples
reactjsreact-hooksusecallback

React useEffect dependency or useCallback seem to cause re-renders


I have a useEffect function that takes an image, resizes it for display and makes it ready for upload. While this works I get the React warning to list the function in the useEffect dependency list. But when I do this it causes continuous re-renders even when moving the function to a useCallback. The code is as follows ...

import React, { useEffect, useRef, useCallback } from 'react'

const Canvas = (props) => {

   const { width = 180, height = 220, img, onChange = null, setFile = null} = props

   const canvas = useRef(null)

   const setBlobFile = useCallback(
       (blob) => {
           setFile(blob)
       },
       [setFile]
   )

   useEffect(() => {

       if (img) {
           console.log("Canvas useEffect ...", img)
           var image = new Image()

           image.src = img
           image.onload = () => {

               canvas.current.getContext("2d")
               canvas.current.getContext("2d").clearRect(0, 0, width, height);
               canvas.current.getContext("2d").drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)
               canvas.current.toBlob((blob) => {
                   setBlobFile(blob)
               })

           }

       }
      
   }, [img, width, height, setBlobFile])

   const onFileSelect = (e) => {

       const objectURL = URL.createObjectURL(e.target.files[0])
       onChange(objectURL)

   }

   return (

       <div onClick={onClick}>
           <label htmlFor='upload'>
               <canvas ref={canvas} width={width} height={height} />
               <input type='file' id='upload' onChange={(e) => onFileSelect(e)} style={{ display: 'none' }} />
           </label>
       </div>

   )

}

export { Canvas as default }

Parent Component provides the setFile routine to set the file for upload

...

const [selectedFile, setSelectedFile] = useState(new File([""], ""))
const setFile = (aBlob) => {

        var img = new Image()
        img = aBlob
        setSelectedFile(new File([aBlob], "image.png", {
            type: 'image/png',
          }))
        
    }
...

Any ideas why I'm seeing this behaviour? I thought the useCallback() approach was supposed to stop the re-renders but it seems to create them. How can I avoid the warning but also stop the re-rendering behaviour? Thanks for taking a look.


Solution

  • You have to use useCallback where the function is created, not where it is used.

       const setBlobFile = useCallback(
           (blob) => {
               setFile(blob)
           },
           [setFile]
       )
    

    in Canvas is useless for preventing rerenders when setFile changes because setBlobFile will still change when setFile changes, since setFile is passed as a dependency.


    Instead you need to to use useCallback in the parent component where setFile is created in the first place:

    const [selectedFile, setSelectedFile] = useState(new File([""], ""))
    const setFile = useCallback((aBlob) => {
        var img = new Image()
        img = aBlob
        setSelectedFile(new File([aBlob], "image.png", {
            type: 'image/png',
        }))
    }, [setSelectedFile]);
    

    This is will work as expected because useState guarantees that the setter function (setSelectedFile) will never change between renders and therefore setFile will never change between renders.