stevendesu
stevendesu

Reputation: 16831

How to avoid a functional cyclic dependency in JavaScript?

There's a particular JavaScript pattern that has always bothered me, and I never really figured out the proper way to solve it. Instead I usually just ignore it because 99% of JavaScript interpreters support function hoisting so no run-time errors arise.

Consider the following:

function onOpen()
{
    console.log("Connected!");
    ws.removeEventListener("open", onOpen);
    ws.removeEventListener("error", onError);
}

function onError()
{
    console.log("Failed to connect!");
    ws.removeEventListener("message", onMessage);
    ws.removeEventListener("error", onError);
}

var ws = new WebSocket("...");
ws.addEventListener("open", onOpen);
ws.addEventListener("error", onError);

In this code, within the onOpen function, I'm referencing onError before onError has been defined lower in the code. This isn't actually a problem, because the onOpen method won't run until after onError has been defined, but it's still bad practice and trips up ESLint's no-use-before-define rule

In a more general sense, this is an error that will arise whenever two functions exist that each need to reference each other:

function a(x) {
    return x === 1 ? b(x) : 2;
}
function b(x) {
    return x === 2 ? a(x) : 1;
}

Is there a design pattern for eliminating this circular dependency? In my simple generic example the easy solution is "only have one function":

function a(x) {
    return x === 1 ? 1 : 2;
}

However when binding event listeners this doesn't always seem possible.

Upvotes: 1

Views: 971

Answers (2)

Bergi
Bergi

Reputation: 664970

It's still bad practice and trips up ESLint's no-use-before-define rule

No, it's not a bad practice, it's very much a necessity. It is the preferred pattern when you have a circular functional dependency. Configure your ESLint accordingly (with { "functions": false }).

Is there a design pattern for eliminating this circular dependency?

Not really. There are several workarounds however, such as declaring the function up-front with a var (which is enough to make ESLint happy). Alternatively, you could pass a reference to the function around via parameters:

function _a(x, f) {
    return x === 1 ? f(x) : 2;
}
function b(x) {
    return x === 2 ? _a(x, b) : 1;
}
function a(x) {
    return _a(x, b);
}

Other crazy hacks with closures, following up on the Y combinator idea, are imaginable. This is not really suitable for your event listener scenario though.

Upvotes: 3

Alex Yokisama
Alex Yokisama

Reputation: 1041

I don't think this is a real problem, because usually everything works fine. Also you worry about the fact that you mention onError before it is declared, but you don't care about mentioning ws. But if yous still need to solve this, I hope this approach will do the work.

var eventListenerListOnOpen = [];
var eventListListenerOnError = [];
var eventListListenerOnMessage = [];

var ws;

function removeListedEventListeners(object, eventName, eventListenerList) {
  eventListenerList.forEach(function(listener) {
    object.removeEventListener(eventName, listener);
  });
}

function onOpen() {
  removeListedEventListeners(ws, "open", eventListenerListOnOpen);
  removeListedEventListeners(ws, "error", eventListenerListOnError);
}

function onError() {
  removeListedEventListeners(ws, "message", eventListenerListOnMessage);
  removeListedEventListeners(ws, "error", eventListenerListOnError);
}

ws = new WebSocket("...");
ws.addEventListener("open", onOpen);
eventListenerListOnOpen.push(onOpen);
ws.addEventListener("error", onError);
eventListenerListOnError.push(onError);

Upvotes: 2

Related Questions