Search code examples
javascriptreactjsnext.jsxtermjs

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


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.


Solution

  • 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.