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 Canvas
to 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.
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;