I have a logic that need to reactively run some function
if a this.variableName is changed inside or outside the class

the problem is with this logic, getter doesn't work, and I get undefined.

not only this, but also maximum stack error.
because is like: set the this inside the setter, that re-set the this, that re-call another time the setter, that set the this, and so on... (but this isn't what I meant)


Semplified version, without parameters, and so, just the minimal example of the bug.

class MyClass {
  constructor() {
     this.isOn = false;
  }

  set isOn(value) {
    this.isOn = value;
    // another functions that do stuff. you can put here a bunch of console.logs if you want just for the demo
  }

  // the idea is another, but i will show you the simpliest method, I could imagine, just to make you get the idea
  
  turnOn() {
     this.isOn = true;
  }
 
  turnOff() {
     this.isOn = false;
  }

  // using this approach, 
  // I don't need to copy paste the same functions in this 2 methods, 
  // so I just need to care about the var, 
  // without using a imperative approach, 
  // but a declarative approach
}

I know we can use _isOn,
but this is literally the opposite of what we want,
because I want the setter to be the same name of the getter,
and still do the logic on setter.

hope you get my idea. thanks

3

There are 3 best solutions below

6
Konrad On BEST ANSWER

You can use private properties

class MyClass {
  #isOn = false;

  set isOn(value) {
    this.#isOn = value;
  }
  
  get isOn() {
    return this.#isOn
  }

  turnOn() {
     this.isOn = true;
  }
 
  turnOff() {
     this.isOn = false;
  }
}

const x = new MyClass

x.isOn = true

console.log(x.isOn)

// This will throw an error:
// console.log(x.#isOn)

0
Oskar Grosser On

As Konrad has commented, you cannot reference two distinct objects with one identifier. Instead, you could use internal members when referencing from inside.

Alternatively, if we separate the data from the class, we could instance two objects: One where changing the data (e.g. through a proxy) has a side-effect, and the other that changes directly.

Example:

class MyClass {
  #data = null;

  constructor(data) {
    this.#data = data;
  }
  
  set isOn(value) {
    this.#data.isOn = value;
  }
  get isOn() {
    return this.#data.isOn;
  }
  
  turnOn() {
    this.isOn = true;
  }

  turnOff() {
    this.isOn = false;
  }
}

const data = {
  isOn: false
};

const dataProxy = new Proxy(data, {
  get(target, prop, receiver) {
    if (prop === "isOn") {
      console.log("get side-effect");
    }
    return Reflect.get(...arguments);
  },
  set(target, prop, value, receiver) {
    if (prop === "isOn") {
      console.log("set side-effect");
    }
    return Reflect.set(...arguments);
  }
});

const directObject = new MyClass(data);
const indirectObject = new MyClass(dataProxy);

console.log("Direct isOn:", directObject.isOn);
console.log("Direct turnOn()");
directObject.turnOn();

console.log("Indirect isOn:", indirectObject.isOn);
console.log("Indirect turnOff()");
indirectObject.turnOff();

Here, we have two objects of class MyClass that act on the same data, one of them through a proxy. The proxied MyClass's methods will cause side-effects.

This allows for one implementation for the behaviour (one MyClass class), and one set of data. The proxy allows for controlling access and (as you wanted) causing side-effects when accessing.

1
Peter Seliger On

From my above comments ...

There is anyhow a design flaw, one either uses a get/set solution for what gets exposed as a single public property (here isOn) or one does use two differently named public methods (here turnOn/turnOff). And since both solutions implement at least set functionality, both need to achieve this by changing/controlling a somehow private value (be it a private instance field or a local variable or maybe even a WeakMap based solution). What the OP's code so far tries to achieve, and also what the accepted answer offers, is not necessary. Already having two explicit setters like turnOn/turnOff which both do change a public isOn property is utterly wrong, at least it's error prone. Worse, isOn already has been implemented via get/set. Either go with the latter or with the former and either variant implemented properly. Do not mix both; it does not make any sense.

Taking into account the above said, there are following two code examples.

The 1st one focusses on the get/set implementation of a single public isOn property, whereas the 2nd chooses the approach of explicit methods like turnOn/turnOff in combination with a single get for the sole public isOn property.

Each approach offers two implementations, one choosing private instance field(s) and the other one being based on encapsulated local variables.

class SwitchStateWithGetSetAndPrivateFields {
  #isOn;
  #handleSwitchChange;

  constructor(switchDefault = false, onSwitchChange) {
    this.#isOn = Boolean(switchDefault);
    this.#handleSwitchChange = onSwitchChange;
  }
  set isOn(value) {
    if ('boolean' === typeof value) {

      if (this.#isOn !== value) {

        this.#isOn = value;
        this.#handleSwitchChange(this);
      }
    } else {
      throw new TypeError('The assigned value needs to be a boolean type.');
    }
  }
  get isOn() {
    return this.#isOn;
  }
}

class SwitchStateWithGetSetAndLocalValues {

  constructor(switchDefault = false, handleSwitchChange) {
    let isOn = Boolean(switchDefault);

    Object.defineProperty(this, 'isOn', {
      set(value) {
        if ('boolean' === typeof value) {

          if (isOn !== value) {

            isOn = value;
            handleSwitchChange(this);
          }
        } else {
          throw new TypeError('The assigned value needs to be a boolean type.');
        }
      },
      get() {
        return isOn;
      },
      enumerable: true,
    });
  }
}

function logSwitchStateOfBoundFieldtype(fieldtype, instance) {
  console.log({ [`${ fieldtype }IsOn`]: instance.isOn });
}

const onOffStateRadio =
  new SwitchStateWithGetSetAndPrivateFields(
    true, logSwitchStateOfBoundFieldtype.bind(null, 'radio')
  );
const onOffStateText =
  new SwitchStateWithGetSetAndLocalValues(
    false, logSwitchStateOfBoundFieldtype.bind(null, 'text')
  );

document
  .querySelector('form')
  .addEventListener('submit', evt => {
    evt.preventDefault();

    const booleanMap = {
      'true': true,
      'false': false,
      '': false,
    };
    const formElements = evt.currentTarget.elements;

    const radioValue = booleanMap[formElements['is_on'].value];
    const textValue = booleanMap[formElements['is_on_text'].value.trim().toLowerCase()];

    onOffStateRadio.isOn = radioValue;
    onOffStateText.isOn = textValue;
  });

logSwitchStateOfBoundFieldtype('radio', onOffStateRadio);
logSwitchStateOfBoundFieldtype('text', onOffStateText);
body { margin: 0; }
form { width: 40%; }
fieldset { margin: 5px 0; }
form > label { display: block; margin: 5px 0 10px 14px; }
form > label > input, form > button { float: right; }
.as-console-wrapper { left: auto!important; min-height: 100%; width: 59%; }
<form>

  <fieldset>
    <legend>isOn:</legend>
    <label>
      <span>true</span>
      <input type="radio" name="is_on" value="true" checked/>
    </label>
    <label>
      <span>false</span>
      <input type="radio" name="is_on" value="false"/>
    </label>
  </fieldset>

  <label>
    <span>isOn:</span>
    <input type="text" name="is_on_text" placeholder="whatever"/>
  </label>

  <button type="submit">apply values</button>
</form>

class SwitchStateWithExplicitMethodsSingleGetAndPrivateFields {
  #isOn;
  #handleSwitchChange;

  constructor(switchDefault = false, onSwitchChange) {
    this.#isOn = Boolean(switchDefault);
    this.#handleSwitchChange = onSwitchChange;
  }
  get isOn() {
    return this.#isOn;
  }

  turnOn() {
    if (this.#isOn === false) {

      this.#isOn = true;
      this.#handleSwitchChange(this);
    }
  }
  turnOff() {
    if (this.#isOn === true) {

      this.#isOn = false;
      this.#handleSwitchChange(this);
    }
  }
}

class SwitchStateWithExplicitMethodsSingleGetAndLocalValues {

  constructor(switchDefault = false, handleSwitchChange) {
    let isOn = Boolean(switchDefault);

    Object.defineProperty(this, 'isOn', {
      get() {
        return isOn;
      },
      enumerable: true,
    });

    this.turnOn = function turnOn() {
      if (isOn === false) {

        isOn = true;
        handleSwitchChange(this);
      }
    }
    this.turnOff = function turnOff() {
      if (isOn === true) {

        isOn = false;
        handleSwitchChange(this);
      }
    }
  }
}

function logSwitchStateOfBoundFieldtype(fieldtype, instance) {
  console.log({ [`${ fieldtype }IsOn`]: instance.isOn });
}

const onOffStateRadio =
  new SwitchStateWithExplicitMethodsSingleGetAndPrivateFields(
    true, logSwitchStateOfBoundFieldtype.bind(null, 'radio')
  );
const onOffStateText =
  new SwitchStateWithExplicitMethodsSingleGetAndLocalValues(
    false, logSwitchStateOfBoundFieldtype.bind(null, 'text')
  );

document
  .querySelector('form')
  .addEventListener('submit', evt => {
    evt.preventDefault();

    const booleanMap = {
      'true': true,
      'false': false,
      '': false,
    };
    const formElements = evt.currentTarget.elements;

    const radioValue = booleanMap[formElements['is_on'].value];
    const textValue = booleanMap[formElements['is_on_text'].value.trim().toLowerCase()];

    onOffStateRadio[radioValue && 'turnOn' || 'turnOff']();
    onOffStateText[textValue && 'turnOn' || 'turnOff']();
  });

logSwitchStateOfBoundFieldtype('radio', onOffStateRadio);
logSwitchStateOfBoundFieldtype('text', onOffStateText);
body { margin: 0; }
form { width: 40%; }
fieldset { margin: 5px 0; }
form > label { display: block; margin: 5px 0 10px 14px; }
form > label > input, form > button { float: right; }
.as-console-wrapper { left: auto!important; min-height: 100%; width: 59%; }
<form>

  <fieldset>
    <legend>isOn:</legend>
    <label>
      <span>true</span>
      <input type="radio" name="is_on" value="true" checked/>
    </label>
    <label>
      <span>false</span>
      <input type="radio" name="is_on" value="false"/>
    </label>
  </fieldset>

  <label>
    <span>isOn:</span>
    <input type="text" name="is_on_text" placeholder="whatever"/>
  </label>

  <button type="submit">apply values</button>
</form>