Event listeners crash InDesign when rebuilding a UXP plugin

,

Reproduction steps:

  • Get the react-starter sample project for InDesign;
  • Add an event listener when the Home component is rendered;
import React, { useEffect } from "react";
import { app } from "indesign";

import { WC } from "./WC.jsx";

import "./Home.css";

export const Home = () => {
  useEffect(() => {
    app.addEventListener("afterSelectionChanged", function (event) {
      console.log("Selection changed");
    });
  }, []);

  return (
    <div>
      <WC>
        <p className="display">
          Congratulations! You just created your first React Plugin.
        </p>
      </WC>
    </div>
  );
};
  • Run the watch script and load and watch the plugin into InDesign using the UXP dev tools;
  • Force a plugin rebuild;
  • InDesign crashes when the plugin is reloaded.

It would be nice to get this sorted out. This won’t be an issue when the plugin is deployed but it’s annoying to developers.

3 Likes

I can confirm this. I’m not quite sure about “won’t be an issue when the plugin is deployed”, though. It won’t consistently break the plugin once deployed, but I’ve had several weird crashes that weren’t reproducible but seemed to correlate with using InDesign event listeners. I suspect this also played a role in

I can confirm that it consistently happens when rebuilding the plugin, though.

I’m currently replicating this kind of functionality (in all of my InDesign-based projects) to re-evaluating any dynamic state on a setInterval() loop. Far from ideal, but the only way I found that doesn’t seem to increase InDesign crashes (or at least: not significantly enough to confirm it during my tests).

Overall (and this isn’t just limited to events), InDesign seems to become unstable when more complex event loop interactions take place between UXP and InDesign APIs. This also, for example, affects interactions between app.doScript() and promises / async operations. Definitely something Adobe needs to fix if they want UXP to be a viable option for broader InDesign extensibility.

1 Like

This is what I ended up doing to avoid these crashes using event listeners.

export const Home = () => {
  const handleSelectionChanged = () => {
    console.log("Selection changed");
  };

  useEffect(() => {
    const eventListener = app.eventListeners.item("myEventName");
    if (eventListener.isValid) {
      // app.removeEventListener("afterSelectionChanged", handleSelectionChanged); // this will crash InDesign
      eventListener.remove();
    }
    const action = app.addEventListener(
      "afterSelectionChanged",
      handleSelectionChanged
    );
    if (action.isValid) action.name = "myEventName";
  }, []);

  return (
    ...
  );
};

It would be nice if someone from the InDesign team could explain what is the difference between using app.removeEventListener and eventListener.remove.

2 Likes

It would be nice if someone from the InDesign team could explain what is the difference between using app.removeEventListener and eventListener.remove.

From what I’ve observed is that the EventListeners collection contains ALL app events registered in InDesign. The remove method on that allows you to unregister any listener, even those not registered by your script or plugin! In fact you can even disable built-in handlers (for “homeScreenVisibilityChange”) this way… icky stuff.

The app.removeEventListener has an additional safety net because it requires you to specify the original handler function, which only the original script/plugin has the reference of. But this only works for as long as the script/plugin is still loaded, after reload the reference is no longer valid, meaning that you cannot remove it that way. This dangling reference to a function which no longer exists is probl. also why indesign crashes (well the cleanup part might be missing somewhere).

@pedro Your solution works nicely, but there is no way of knowing if the myEventName is your event handler - to be super-sure you should use a super-unique name

3 Likes