'context.putImageData() Not working in React

Im trying to create a drawing application in react, its working for the most part. But when i try add an undo button it doesnt work.

I try make the undo button with this finishDrawing() function, where i use getImageData and store it in an array called restore_array

    const finishDrawing = () => {
        //console.log(contextRef.current)
        contextRef.current.closePath()
        setIsDrawing(false)
        //restore_array.push(contextRef.current.getImageData(0, 0,400,256+80))
        setIndex_array(index_array + 1)
        setRestore_Array((prevState) => { return [...prevState, contextRef.current.getImageData(0, 0,400,256+80)]})
        console.log(restore_array, restore_array.length)
        
    }

then when a Undo button is clicked i try to reload this image in the restore_array with a useEffect which should re-render the entree canvas with the previous image

    function undoLast() {
        console.log(restore_array[index_array], index_array, restore_array)
        if (index_array > -1) {
            setIndex_array(index_array-1)
            restore_array.pop()
            setLines(!lines)

        }
        //contextRef.current.putImageData(restore_array[index_array-1], 0, 0)   
    }

and

    useEffect(() => {
        console.log('use effect 3c')
        console.log(restore_array)
        const canvas = canvasRef.current;
        const context = canvas.getContext("2d")
        context.lineWidth = thick
        context.lineCap = "round"
        context.strokeStyle = colour
        if (restore_array.length === 0) {
            console.log('empty')
            return
        }
        //context.clearRect(0,0,400,256+80)
        //context.fillRect(0,0,400,256+80)
        context.putImageData(restore_array[index_array], 0, 0, 0, 0, 400, 256+80)
        contextRef.current = context
    }, [lines])

Does anyone know why my putImageData isnt working in this case?

Heres my entire code for reference

import React, { useEffect, useRef, useState } from "react";



function DrawCanvas() {

    const canvasRef = useRef(null)
    const contextRef = useRef(null)
    const [isDrawing, setIsDrawing] = useState(false)
    const [colour, setColour] = useState('black')
    const [prcolour, setPrColour] = useState(null)
    const [thick, setThick] = useState(3)
    const [image, setImage] = useState(null)
    const [restore_array, setRestore_Array] = useState([])
    const [index_array, setIndex_array] = useState(-1)
    const [clear_, setClear_] = useState(true)
    const [lines, setLines] = useState(true)
    //let restore_array = []





    useEffect(() => {
        const canvas = canvasRef.current;
        canvas.width = window.innerWidth/1.5 ;
        canvas.height = window.innerHeight/1.5 ;
        canvas.style.width = `${window.innerWidth/3}px`;
        canvas.style.height = `${window.innerHeight/3}px`;
        const context = canvas.getContext("2d")
        context.scale(2,2)
        context.lineCap = "round"
        context.lineWidth = 3
        context.strokeStyle = "black"
        //const img = DogImage
        context.fillStyle = 'gray'
        context.fillRect(0,0,400,256+80)
        contextRef.current = context
        
    }, [])




    useEffect(() => {
        console.log('use effect 1a')
        const canvas = canvasRef.current;
        const context = canvas.getContext("2d")
        context.lineWidth = thick
        context.lineCap = "round"
        context.strokeStyle = colour
        contextRef.current = context
        document.getElementById(colour).style.borderColor = 'gray'
        if(prcolour === null) {
            return
        }
        document.getElementById(prcolour).style.borderColor = 'black'
        document.getElementById(prcolour).style.borderWidth = '3px'

    }, [colour])

    useEffect(() => {
        console.log('use effect 2b')
        const canvas = canvasRef.current;
        const context = canvas.getContext("2d")
        context.lineWidth = thick
        context.lineCap = "round"
        context.strokeStyle = colour
        const Dogimage = new Image();
        Dogimage.src = '../../images/dog-without-labels.png'
        Dogimage.onLoad = () => {
            console.log('load image')
            context.current.drawImage(Dogimage, 0, 0,400,256+80);
        }
        contextRef.current = context
    }, [thick])

    useEffect(() => {
        console.log('use effect 3c')
        console.log(restore_array)
        const canvas = canvasRef.current;
        const context = canvas.getContext("2d")
        context.lineWidth = thick
        context.lineCap = "round"
        context.strokeStyle = colour
        if (restore_array.length === 0) {
            console.log('empty')
            return
        }
        //context.clearRect(0,0,400,256+80)
        //context.fillRect(0,0,400,256+80)
        context.putImageData(restore_array[index_array], 0, 0, 0, 0, 400, 256+80)
        contextRef.current = context
    }, [lines])

    useEffect(() => {
        console.log('use effect 4d')
        const canvas = canvasRef.current;
        const context = canvas.getContext("2d")
        //contextRef.fillStyle = 'gray'
        context.lineWidth = thick
        context.lineCap = "round"
        context.strokeStyle = colour
        context.clearRect(0,0,400,256+80)
        context.fillRect(0,0,400,256+80)
        contextRef.current = context
    }, [clear_])

    


    function undoLast() {
        console.log(restore_array[index_array], index_array, restore_array)
        if (index_array > -1) {
            setIndex_array(index_array-1)
            restore_array.pop()
            setLines(!lines)

        }
        //contextRef.current.putImageData(restore_array[index_array-1], 0, 0)   
    }

    const clearCanvas = () => {
        setClear_(!clear_)
    }



    const startDrawing = ({nativeEvent}) => {
        const {offsetX, offsetY} = nativeEvent
        contextRef.current.beginPath()
        contextRef.current.moveTo(offsetX, offsetY)
        setIsDrawing(true)

    }

    const finishDrawing = () => {
        //console.log(contextRef.current)
        contextRef.current.closePath()
        setIsDrawing(false)
        //restore_array.push(contextRef.current.getImageData(0, 0,400,256+80))
        setIndex_array(index_array + 1)
        setRestore_Array((prevState) => { return [...prevState, contextRef.current.getImageData(0, 0,400,256+80)]})
        console.log(restore_array, restore_array.length)
        
    }

    const draw = ({nativeEvent}) => {
        if (!isDrawing) {
            return
        }
        const {offsetX, offsetY} = nativeEvent;
        contextRef.current.lineTo(offsetX, offsetY)
        contextRef.current.stroke()
    }

    const colourChange = (value) => {
        setPrColour(colour)
        setColour(value)
    }

    const thickChange = (event) => {
        setThick(event)
    }
      
      
    return (
    <div>
        <div className="left">
            
            
                <canvas className="canvas" onMouseDown={startDrawing}
                        onMouseUp={finishDrawing}
                        onMouseMove={draw}
                        ref={canvasRef}/>
        </div>
        <div className="right">
            <h5><dt>Chart Draw</dt></h5>
            <p>Use your finger or your mouse to draw directly on the chart opposite.</p>

            <button className="draw">Draw</button>
            <button className="select">Select</button>

            <p><dt>Line thickness: {thick} </dt> </p>
            <input className="slider" type="range" min="1" max="19" value={thick} class="slider" id="myRange" step="1" onChange={(event)=>thickChange(event.target.value)}></input>
            
            <button className='red' id='red'onClick={() => colourChange('red')}>  </button>
            <button className='black' id='black'  onClick={() => colourChange('black')}>  </button>
            <button className='yellow' id='rgba(204,153,0,255)' onClick={() => colourChange('rgba(204,153,0,255)')}>  </button>
            <button className='blue' id='rgba(1,102,255,255)' onClick={() => colourChange('rgba(1,102,255,255)')}>  </button>
            <button className="undoButton" onClick={()=>undoLast()}> <dt> Undo/Back a step</dt></button>
            <button className="undoButton" onClick={()=>clearCanvas()}> <dt> Clear canvas</dt></button>
            <button className="savecloseButton"> <dt>Save and Close</dt></button>

        </div>
    </div>
    )

}

export default DrawCanvas;



Solution 1:[1]

Your code works "OK" for me...
I think it's obvious that your putImageData in function undoLast is commented.
I'm assuming that was intentional, not the actual problem.


We can see it working here:
enter image description here

Yes only part is getting the undo.
That is because you have hardcoded values in the getImageData:

setRestore_Array((prevState) => { 
  return [...prevState, contextRef.current.getImageData(0, 0,400,256+80)]
})

That only gets a portion of the canvas, so only that portion is getting restored...
if you needed the entire canvas, change that to use the canvas dimensions.
Something like:

setRestore_Array((prevState) => { 
  return [...prevState, contextRef.current.getImageData(0, 0, canvasRef.current.width, canvasRef.current.height)]
})

Also you should look at what @Kaiido mentioned, he has a good point:

storing an Array of drawing commands will cause your memory to explode

For your approach using image data my recommendation is to limit the undo to a fix amount, like 10 and discard older, that will guarantee a cap on memory consumption and prevent any out of memory errors.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1