XtermJS "cannot read properties of undefined (reading 'dimensions') v5.3 on NextJs14

69 Views Asked by At

EDIT: More information

 useEffect(() => {
    // let terminal;
    if (terminalRef.current) {
      const terminal = new Terminal({
        fontFamily: "Menlo, Monaco, monospace",
        // fontSize: 12,
        // cursorBlink: true,
        theme: {
          background: "#001e4821",
          foreground: "#f0f0f0",
        },
      });

      terminal.open(terminalRef.current);
      setTerm(terminal);
    }

    // return () => {
    //   terminal.dispose();
    // };
  }, []);

initializing Terminal after the ref is defined will remove the error. However, I can no longer return a cleanup function (because terminal is instantiated inside the if statement)

useEffect(() => {
    let terminal; // Declare variable in the useEffect scope but outside the if block

    if (terminalRef.current) {
      terminal = new Terminal({
        // Instantiate the terminal inside the if block
        fontFamily: "Menlo, Monaco, monospace",
        theme: {
          background: "#001e4821",
          foreground: "#f0f0f0",
        },
      });

      terminal.open(terminalRef.current);
      setTerm(terminal);
    }

    return () => {
      // Cleanup function
      if (terminal) {
        terminal.dispose(); // Dispose of the terminal instance if it has been created
      }
    };
  }, []);

This doesn't work either. I need to return the cleanup function, otherwise it renders two terminals.

Original post below:

This is the error:

Unhandled Runtime Error
TypeError: Cannot read properties of undefined (reading 'dimensions')

Call Stack
get dimensions
node_modules/xterm/lib/xterm.js (1:103398)
t.Viewport._innerRefresh
node_modules/xterm/lib/xterm.js (1:49940)
eval
node_modules/xterm/lib/xterm.js (1:49741)

this is the code:

"use client";
import React, { useRef, useEffect, useState, useMemo } from "react";
import { Terminal } from "xterm";
import "xterm/css/xterm.css";

const XTerminal: React.FC = () => {
  const terminalRef = useRef<HTMLDivElement | null>(null);
  const [term, setTerm] = useState<Terminal | null>(null);

  useEffect(() => {
    const terminal = new Terminal({
      fontFamily: "Menlo, Monaco, monospace",
      fontSize: 12,
      cursorBlink: true,
      theme: {
        background: "#001e4821",
        foreground: "#f0f0f0",
      },
    });

    if (terminalRef.current) {
      terminal.open(terminalRef.current);
      setTerm(terminal);
    }

    return () => {
      terminal.dispose();
    };
  }, []);

  useEffect(() => {
    if (term) {
//more code
    }
  }, [term]);

  return <div ref={terminalRef}></div>;
};

export default XTerminal;

This is the parent component:

<div className="z-30 px-8 absolute w-100 h-100 left-2 top-1/4">
        <XTerminal />
</div>

I'm using Nextjs14 and using xtermjs v5.3. This is code I wrote in September (using v5.2) that is no longer working. I tried downgrading to v5.2 but the error still appears.

I'm not sure what the issue is.

I thought it might have to do with width/height not being explicitly specified or absolute positioning, but its not the case. I am rendering client side ('use client') so I'm assuming that's not the issue either.

There is no "dimensions" property in the constructor. https://xtermjs.org/docs/api/terminal/classes/terminal/#constructor I noticed a "windowOptions" property but changing these makes no difference to the error. https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/#optional-windowoptions

Any ideas/solutions? Please let me know if you need further clarification.

1

There are 1 best solutions below

0
Bigboss01 On BEST ANSWER

Edit- You will also need to import this dynamically with no ssr. in nextjs 14 the code is as follows:

const XTerminalNoSSR = dynamic(() => import("./terminal/terminal"), {
  ssr: false, // This disables server-side rendering for the import
});
useEffect(() => {
    const terminal = new Terminal({
      fontFamily: "Menlo, Monaco, monospace",
      theme: {
        background: "#001e4821",
        foreground: "#f0f0f0",
      },
    });
    setTerm(terminal);
    return () => {
      if (terminal) {
        terminal.dispose();
      }
    };
  }, []);

  useEffect(() => {
    if (term) {
      if (terminalRef.current) {
        term.open(terminalRef.current);
        setTerm(term);
        term?.write(`hello`);
      }
      const disposable = term.onData((data) => {
        if (data === "\r") {
          term?.write(`\r\n`);
        } else if (data.charCodeAt(0) === 127) {
          term?.write("\b \b");
        } else {
          term?.write(data);
        }
      });

      return () => {
        disposable.dispose();
      };
    }
  }, [term]);

The error seems to indicate that the terminal is being initialized before the ref's dimensions are communicated to it. the offending line of code is 'term.open()'

Therefore, I initialized the terminal and set it on first mount, Then upon 'term' change, conditionally opened the terminal based on the existence of its ref.

I don't know why this works. Probably has to do with the order of the render cycle.