reactjstypescriptcanvasreact-hooksusecallback

Canvas not updating on depency update in React


Using React and Typescript, I am trying to update a canvas based on a provided image with the canvas and file upload DOM elements separated into their own components.

The problem I'm facing is that when a new image is uploaded, the canvas does not update. I have been able to trick it in to updating by getting the page to re-render by doing something like saving a new change to one of the components, prompting my local server to update the page without refreshing.

interface ImageUploadProps { onChange: any }

const ImageUpload = (props: ImageUploadProps) => {
    const [file, setFile] = useState();
    const fileSelectedHandler = (event: any) => { props.onChange(URL.createObjectURL(event.target.files[0])); }
    return (
        <input type="file" name="image" id="uploaded-img" onChange={fileSelectedHandler}/>
    );
}

export default ImageUpload;
interface CanvasProps { url: string, width: number, height: number }

const Canvas = (props: CanvasProps) => {
    const canvasRef = useRef<HTMLCanvasElement>(null);
    useEffect(() => {
        if (!canvasRef.current){ throw new Error("Could get current canvas ref"); }
        const canvas: HTMLCanvasElement = canvasRef.current;
        const ctx: CanvasRenderingContext2D = canvas.getContext("2d")!;
        let image: CanvasImageSource = new Image();
        image.src = props.url;
        ctx.drawImage(img, 0, 0, Number(canvas.width), Number(canvas.height));
    }, [props.url]);

    return (
        <canvas ref={canvasRef} width={props.width} height={props.height}></canvas>
    );
}

export default Canvas;
const App = () => {
    const [url, setUrl] = useState("");
    const handleURLChange = (u: string) => { setUrl(u); }

    return (
        <div>
            <Canvas url={url} width={400} height={400}/>
            <ImageUpload onChange={handleURLChange}/>
        </div>
    );
};

export default App;

I temporarily added an <img src={props.url}> tag to the Canvasto confirm that the component is being updated, which it is, since the image is being show on through that tag every time a new one is updated (as I would expect), while the canvas element itself stays blank the whole time.

I also tried using useCallback() in handleURLChange(), and making the canvasRef into a it's own useCanvas() function like this. Neither change made any impact on the functionality.

I am still learning the ropes of React, so it is possible I am not using useEffect or useRef properly, but from my research they do seem to be correct.

Reproducible example in code sandbox.


Solution

  • The solution I went with was to create a new ref for the previous URL, then the image only changes when the URL has changed by checking an if condition. This causes the image to appear when props.url is updated without causing an infinite loop.

    const Canvas = (props: CanvasProps) => {
      const canvasRef = useRef<HTMLCanvasElement>(null);
      const prevUrlRef = useRef<string>("");
    
      useEffect(() => {
        if (!canvasRef.current) {
          throw new Error("Could not get current canvas ref");
        }
    
        const canvas: HTMLCanvasElement = canvasRef.current;
        const ctx: CanvasRenderingContext2D = canvas.getContext("2d")!;
        const image: CanvasImageSource = new Image();
    
        if (props.url !== prevUrlRef.current) {
          prevUrlRef.current = props.url;
          ctx.clearRect(0, 0, canvas.width, canvas.height);
    
          image.onload = () => {
            canvas.height = canvas.width * (image.height / image.width);
            drawImage(image, ctx, canvas.width, canvas.height);
          };
    
          image.src = props.url;
        }
      }, [props.url]);
    
      return (
        <div>
          <canvas
            ref={canvasRef}
            width={props.width}
            height={props.height}
          ></canvas>
        </div>
      );
    };
    
    export default Canvas;