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.
2
Answers
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.You should add the dependencies of you useEffect in the dependency array. Whenever a dependency changes, React will run the effect and rerender the component. If you leave it empty, the effect will only run at first render
Also, it’s an antipattern to not add dependencies to the dependency array if they exist.
In this case the
url
is a dependency.canvasRef
is not because React guarantees any refs will always return the same ref at every render.After your edit and on closer inspection of your code using the sandbox, unfortunately I don’t think it’s possible to achieve the behavior you want (or it least I can’t think of a way to do it).
You’re referencing an element directly with
useRef
, anduseRef
doesn’t notify us of its changes. This is a problem because the way React works the ref isn’t updated until after the component is rendered.The best you can do is trigger the rerender of other components when changes happen in the ref with
useCallback
in place ofuseRef
. This works because you can "intercept" the changes in the ref and trigger a new render with it. But the problem remains that the ref is only be updated after the render, so you still can’t rerender the ref component itself with this method.For the same reason, forcing a rerender any other way (setting state or using a reducer for instance) will also not work if it’s during the same DOM update.
If there is a way of doing it, I don’t think it’s worth the trouble and the complexity. You’re probably better off using an
img
tag if it fits your requirements.