Search code examples
reactjsfabricjsuse-context

Why I'm getting Uncaught TypeError: Cannot read properties of null (reading 'add') on deployment?


enter image description here

I'm working on a editor app using fabric.js.On localhost, When i click on Add Circle it works fine while on deployment it is causing cannot read properties of null.

Here is the code:

I'm using react context api by which i add objects in canvas and it displays on screen.

FabricCircle.js

import { fabric } from 'fabric';
import ContextCanvas from '../../../context/ContextCanvas';
import { Button } from '@chakra-ui/react';

const FabricTextBox = () => {
  const [canvas] = useContext(ContextCanvas);
  function addTextBox() {
    const textbox = new fabric.Textbox('Click on the Rectangle to move it.', {
      fontSize: 20,
      left: 50,
      top: 100,
      width: 200,
      fill: 'black',
      color: 'white',
      cornerColor: 'blue',

    });
    canvas.add(textbox);
    canvas.requestRenderAll();
  }
  return (
    <>
      <Button
        type="button"
        colorScheme="blue"
        onClick={addTextBox}
        variant={'ghost'}
        _hover={{}}
        _focus={{}}
        _active={{}}
        textColor={'white'}
        fontWeight={'light'}
      >
        Text Field
      </Button>
    </>
  );
};

export default FabricTextBox;

FabricCanvas.js

import React, { useContext, useLayoutEffect } from 'react';
import { fabric } from 'fabric';
import ContextCanvas from '../../context/ContextCanvas';


const FabricCanvas = () => {
  const [canvas, initCanvas] = useContext(ContextCanvas);


  useLayoutEffect(() => {

    return () => {
      initCanvas(new fabric.Canvas('c'));
    };
  }, []);


  return (
    <>
      <canvas
        id="c"
        width={window.innerWidth}
        height={window.innerHeight}
      />
    </>
  )
}
export default FabricCanvas;

ContextCanvas.js

import { fabric } from 'fabric';

const ContextCanvas = createContext();

export function CanvasProvider({ children }) {
  const [canvas, setCanvas] = useState(null);
  const initCanvas = c => {
    setCanvas(c);
    c.renderAll();
  };

  return (
    <ContextCanvas.Provider value={[canvas, initCanvas]}>
      {children}
    </ContextCanvas.Provider>
  );
}

export default ContextCanvas;

Solution

  • I think the error is related to this line in FabricCircle.js

    canvas.add(textbox);
          ^^^^
    

    because your canvas object is null in production.


    Assuming you use React.StrictMode

    Using React.StrictMode

    With Strict Mode starting in React 18, whenever a component mounts in development, React will simulate immediately unmounting and remounting the component:

    Strict mode flow (read more)

    * React mounts the component.
        * Layout effects are created.
        * Effect effects are created.
    * React simulates effects being destroyed on a mounted component.
        * Layout effects are destroyed. [THIS]
        * Effects are destroyed.
    * React simulates effects being re-created on a mounted component.
        * Layout effects are created
        * Effect setup code runs
    

    The step marked with [THIS] is what makes you feel all right in the local environment (but it's an illusion... See why in the next section).


    With that been said:

    Your canvas is initialized null inside useState and in the useLayoutEffect, you are calling the initCanvas method inside the cleanup function (so it will only be called when it's too late in production, while in development with StrictMode act like an init-function although it's a cleanup function).

    useLayoutEffect(() => {
    
        // Your code should be here
    
        return () => { // The returned function is the cleanup function
    
          // This is executed only when disposing the component.
          initCanvas(new fabric.Canvas('c'));
          // But when strict mode is active 
          //  the component is disposed and re-mounted immidiatly.
        };
    }, []);
    

    This is why the local environment works and the production environment doesn't.

    Solution

    Try updating your useLayoutEffect like this:

    useLayoutEffect(() => {
       initCanvas(new fabric.Canvas('c'));
    }, []);
    

    Conclusion

    You should not initialize your state inside a cleanup function.

    In this case, React.StrictMode behavior prevents you from realizing the error (without strict mode, it wouldn't even work in development).

    Since you were initializing the canvas inside the cleanup function, the canvas never get initialized in time (without the strict mode), remaining null, as the error you receive states.