Leveraging AbortController for GoJS Integration
-
Have you ever forgotten to remove an event listener, or accidentally removed the wrong one? Maybe you’ve used an arrow function when registering a listener, only to realize later that you need the exact same reference to unregister it. These common pitfalls can lead to memory leaks and unexpected behaviour in your applications.
Thankfully, AbortController provides a clean and efficient way to manage event listeners and cancel async operations. While it’s often associated with aborting fetch requests [1], it’s also a powerful tool for handling event listeners in a more controlled and maintainable way. Let’s explore how this concept can help us write better, cleaner code.
Web requests and Fetch
Since AbortController is often introduced as a way to cancel web requests, let’s start with how it works alongside fetch.
Typically, when making a request, you might write:const response = await fetch(url);
However, this approach doesn’t provide a way to cancel the request if it’s no longer needed. For example, if a user navigates away or clicks a "Cancel" button, the request will still complete, potentially wasting resources.
But here is where AbortController comes to the rescue. We can modify our code to be cancellable.
const controller = new AbortController(); const signal = controller.signal; const response = await fetch(url, { signal });
Now, if we decide to cancel the request at any point, we can simply call:
controller.abort();
When
abort()
is called, the fetch request is immediately terminated, and the promise rejects with an AbortError. This is particularly useful in scenarios where a user cancels an action, switches pages, or when we want to prevent unnecessary network requests.️ Event listeners in React
Another powerful use case for AbortController is managing event listeners more efficiently. You might find yourself often writing React code like this:
useEffect(() => { window.addEventListener('resize', handleResize) window.addEventListener('hashchange', handleHashChange) window.addEventListener('storage', handleStorageChange) return () => { window.removeEventListener('resize', handleResize) window.removeEventListener('hashchange', handleHashChange) window.removeEventListener('storage', handleStorageChange) } }, [])
While this approach works, manually removing each event listener can become tedious and error-prone, especially as the number of event listeners grows.
Instead, we can simplify our cleanup logic by leveraging AbortController. SinceaddEventListener
supports passing an AbortSignal as an option, we can register multiple event listeners and remove them all at once in the cleanup function:useEffect(() => { const controller = new AbortController(); const { signal } = controller; window.addEventListener('resize', handleResize, { signal }); window.addEventListener('hashchange', handleHashChange, { signal }); window.addEventListener('storage', handleStorageChange, { signal }); return () => controller.abort(); // Removes all listeners at once }, []);
This small change makes event handling in React applications more manageable, especially when dealing with multiple listeners in components that frequently mount and unmount [2].
AbortController with GoJS
Managing window event listeners is one use case, but the same concept can be applied to the GoJS library. According to its documentation, we can register listeners for multiple different events in a similar way to how we handle window events [3].
const listener = (e: go.DiagramEvent) => { const part = e.subject.part; if (!(part instanceof go.Link)) { console.log('Clicked on ' + part.data.text); } }; diagram.addDiagramListener('ObjectSingleClicked', listener);
In this example, clicking on a node logs a message to the console. To remove the event listener, we simply call:
diagram.removeDiagramListener('ObjectSingleClicked', listener);
However, when dealing with multiple diagram events and model change listeners, managing their removal can quickly become complex and difficult to track. Unfortunately, we can’t simply pass an AbortController signal as a third parameter like we do with
window.addEventListener
. However, we can adjust how we register our listeners to leverage the same technique. The key is to listen for the'abort'
event whenever we register a listener, allowing us to clean them up efficiently. Here's how it can be done:function makeAddDiagramEventListener(diagram: go.Diagram) { return ( eventName: go.DiagramEventName, callback: go.DiagramEventHandler, options: { signal?: AbortSignal } = {} ) => { options.signal?.addEventListener("abort", () => { diagram.removeDiagramListener(eventName, callback); }); return diagram.addDiagramListener(eventName, callback); }; }
Here, we define a higher-order function that first takes the diagram we’re working with. Inside, we register the event listener while also checking if an AbortSignal is provided. If a signal is available, we attach a listener to its abort event to ensure the diagram event listener is properly removed when aborted. Using this function, we can register a
'ObjectSingleClicked'
event on our diagram like this:const controller = new AbortController(); makeAddDiagramEventListener(diagram)('ObjectSingleClicked', listener, { signal: controller.signal });
This way we can add multiple event listeners and be sure we remove them all by:
controller.abort();
The same logic can be applied when working with GoJS model change listeners.
The higher-order method for mode changed listener could look like this:function makeAddModelChangedListener(diagram: go.Diagram) { return ( callback: go.ChangedEventHandler, options: { signal?: AbortSignal } = {} ) => { options.signal?.addEventListener("abort", () => { diagram.removeModelChangedListener(callback); }); return diagram.addModelChangedListener(callback); }; }
It's worth noticing that even if we add both diagram event listeners and model changed listeners we still remove them all by calling abort method on the Abort Controller.
Conclusion
By integrating AbortController, we can make our code more maintainable, scalable, and efficient, especially when dealing with multiple event listeners. Instead of manually tracking and removing listeners, we can leverage a single
abort()
call to clean everything up effortlessly.If you're working with GoJS, consider adopting AbortController to keep your codebase clean and prevent potential memory leaks. Small optimizations like this can significantly improve the maintainability and reliability of your applications.
Happy coding!
Source