Listening for variable changes in JavaScript

Viewed 516759

Is it possible to have an event in JS that fires when the value of a certain variable changes? JQuery is accepted.

28 Answers

This question was originally posted in 2009 and most of the existing answers are either outdated, ineffective, or require the inclusion of large bloated libraries:

  • Object.watch and Object.observe are both deprecated and should not be used.
  • onPropertyChange is a DOM element event handler that only works in some versions of IE.
  • Object.defineProperty allows you to make an object property immutable, which would allow you to detect attempted changes, but it would also block any changes.
  • Defining setters and getters works, but it requires a lot of setup code and it does not work well when you need to delete or create new properties.

As of 2018, you can now use the Proxy object to monitor (and intercept) changes made to an object. It is purpose built for what the OP is trying to do. Here's a basic example:

var targetObj = {};
var targetProxy = new Proxy(targetObj, {
  set: function (target, key, value) {
      console.log(`${key} set to ${value}`);
      target[key] = value;
      return true;
  }
});

targetProxy.hello_world = "test"; // console: 'hello_world set to test'

The only drawbacks of the Proxy object are:

  1. The Proxy object is not available in older browsers (such as IE11) and the polyfill cannot fully replicate Proxy functionality.
  2. Proxy objects do not always behave as expected with special objects (e.g., Date) -- the Proxy object is best paired with plain Objects or Arrays.

If you need to observe changes made to a nested object, then you need to use a specialized library such as Observable Slim (which I have published). It works like this:

var test = {testing:{}};
var p = ObservableSlim.create(test, true, function(changes) {
    console.log(JSON.stringify(changes));
});

p.testing.blah = 42; // console:  [{"type":"add","target":{"blah":42},"property":"blah","newValue":42,"currentPath":"testing.blah",jsonPointer:"/testing/blah","proxy":{"blah":42}}]

 

Recently found myself with the same issue. Wanted to listen for on change of a variable and do some stuff when the variable changed.

Someone suggested a simple solution of setting the value using a setter.

Declaring a simple object that keeps the value of my variable here:

var variableObject = {
    value: false,
    set: function (value) {
        this.value = value;
        this.getOnChange();
    }
}

The object contains a set method via which I can change the value. But it also calls a getOnChange() method in there. Will define it now.

variableObject.getOnChange = function() {
    if(this.value) {
        // do some stuff
    }
}

Now whenever I do variableObject.set(true), the getOnChange method fires, and if the value was set as desired (in my case: true), the if block also executes.

This is the simplest way I found to do this stuff.

Easiest way I have found, starting from this answer:

// variable holding your data
const state = {
  count: null,
  update() {
    console.log(`this gets called and your value is ${this.pageNumber}`);
  },
  get pageNumber() {
    return this.count;
  },
  set pageNumber(pageNumber) {
    this.count = pageNumber;
    // here you call the code you need
    this.update(this.count);
  }
};

And then:

state.pageNumber = 0;
// watch the console

state.pageNumber = 15;
// watch the console

In my case, I was trying to find out if any library I was including in my project was redefining my window.player. So, at the begining of my code, I just did:

Object.defineProperty(window, 'player', {
  get: () => this._player,
  set: v => {
    console.log('window.player has been redefined!');
    this._player = v;
  }
});

Based On akira's answer I added that you can manipulate the dom through the listerner.

https://jsfiddle.net/2zcr0Lnh/2/

javascript:

x = {
  aInternal: 10,
  aListener: function(val) {},
  set a(val) {
    this.aInternal = val;
    this.aListener(val);
  },
  get a() {
    return this.aInternal;
  },
  registerListener: function(listener) {
    this.aListener = listener;
  }
}

x.registerListener(function(val) {
document.getElementById('showNumber').innerHTML = val;
});


x.a = 50;

function onClick(){
x.a = x.a + 1;
}

html:

<div id="showNumber">
 
</div>


<button onclick="onClick()">
click me to rerender
</button>

The registerListener method is fired when the variable x.a changes.

With the help of getter and setter, you can define a JavaScript class that does such a thing.

First, we define our class called MonitoredVariable:

class MonitoredVariable {
  constructor(initialValue) {
    this._innerValue = initialValue;
    this.beforeSet = (newValue, oldValue) => {};
    this.beforeChange = (newValue, oldValue) => {};
    this.afterChange = (newValue, oldValue) => {};
    this.afterSet = (newValue, oldValue) => {};
  }

  set val(newValue) {
    const oldValue = this._innerValue;
    // newValue, oldValue may be the same
    this.beforeSet(newValue, oldValue);
    if (oldValue !== newValue) {
      this.beforeChange(newValue, oldValue);
      this._innerValue = newValue;
      this.afterChange(newValue, oldValue);
    }
    // newValue, oldValue may be the same
    this.afterSet(newValue, oldValue);
  }

  get val() {
    return this._innerValue;
  }
}

Assume that we want to listen for money changes, let's create an instance of MonitoredVariable with initial money 0:

const money = new MonitoredVariable(0);

Then we could get or set its value using money.val:

console.log(money.val); // Get its value
money.val = 2; // Set its value

Since we have not defined any listeners for it, nothing special happens after money.val changes to 2.

Now let's define some listeners. We have four listeners available: beforeSet, beforeChange, afterChange, afterSet. The following will happen sequentially when you use money.val = newValue to change variable's value:

  1. money.beforeSet(newValue, oldValue);
  2. money.beforeChange(newValue, oldValue); (Will be skipped if its value not changed)
  3. money.val = newValue;
  4. money.afterChange(newValue, oldValue); (Will be skipped if its value not changed)
  5. money.afterSet(newValue, oldValue);

Now we define afterChange listener which be triggered only after money.val has changed (while afterSet will be triggered even if the new value is the same as the old one):

money.afterChange = (newValue, oldValue) => {
  console.log(`Money has been changed from ${oldValue} to ${newValue}`);
};

Now set a new value 3 and see what happens:

money.val = 3;

You will see the following in the console:

Money has been changed from 2 to 3

For full code, see https://gist.github.com/yusanshi/65745acd23c8587236c50e54f25731ab.

Please guys remember the initial question was for VARIABLES, not for OBJECTS ;)

in addition to all answers above, I created a tiny lib called forTheWatch.js, that use the same way to catch and callback for changes in normal global variables in javascript.

Compatible with JQUERY variables, no need to use OBJECTS, and you can pass directly an ARRAY of several variables if needed.

If it can be helpful... : https://bitbucket.org/esabora/forthewatch
Basically you just have to call the function :
watchIt("theVariableToWatch", "varChangedFunctionCallback");

And sorry by advance if not relevant.

The question is about variables, not object properties! So my approach is to take advantage of the window object, with its custom getters/setters, and then use/change the variable like a "normal" variable (not like an object property).

The simplest way is that of @José Antonio Postigo in his answer (i voted that answer). What I'd like to do here, is to reduce that to an even simpler "creator" function (so even someone that does not understand object getters/setters can easily use it).

A live example is here: https://codepen.io/dimvai/pen/LYzzbpz

This is the general "creator" function you must have as is:

let createWatchedVariable = (variableName,initialValue,callbackFunction) => {
    // set default callback=console.log if missing
    callbackFunction ??= function(){console.log(variableName+" changed to " + window[variableName])};
    // the actual useful code:
    Object.defineProperty(window, variableName, {
      set: function(value) {window["_"+variableName] = value; callbackFunction()},
      get: function() {return window["_"+variableName]}
    });
    window[variableName]=initialValue??null;
};

Then, instead of declaring the variable using var or let, use this:

// 1st approach - default callback//    
createWatchedVariable ('myFirstVariable',12);  
// instead of: let myFirstVariable = 12;

Or, in order to use your custom callback (instead of the default console.log) use:

// 2nd approach - set a custom callback//
var myCallback = ()=>{/*your custom code...*/}
// now use callback function as the third optional argument
createWatchedVariable('mySecondVariable',0,myCallback);

That's it! Now, you can change it like a "normal" variable:

myFirstVariable = 15;      // logs to console
myFirstVariable++;         // logs to console
mySecondVariable = 1001;   // executes your custom code
mySecondVariable++;        // executes your custom code

The solution of @akira and @mo-d-genesis can be further simplified because the DOM manipulation does not depend on state in this example:

CodePen

const render = (val) => {
  document.getElementById("numberDiv").innerHTML = val;
};

state = {
  _value_internal: undefined,
  set value(val) {
    // 1. set state value
    this._value_internal = val;
    // 2. render user interface
    render(val);
  },
  get value() {
    return this._value_internal;
  },
};

const onClick = () => {
  state.value = state.value + 1; // state change leads to re-render!
};

// set default value
state.value = 0;

The corresponding html:

<div id="numberDiv"></div>

<button onclick="onClick()">
  Click to rerender
</button>

Remarks:

  1. I renamed variables and functions to better reflect their semantics.
  2. FYI: Svelte offers a very similar reactive behavior by changing variables

It's not directly possible.

However, this can be done using CustomEvent: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent

The below method accepts an array of variable names as an input and adds event listener for each variable and triggers the event for any changes to the value of the variables.

The Method uses polling to detect the change in the value. You can increase the value for timeout in milliseconds.

function watchVariable(varsToWatch) {
    let timeout = 1000;
    let localCopyForVars = {};
    let pollForChange = function () {
        for (let varToWatch of varsToWatch) {
            if (localCopyForVars[varToWatch] !== window[varToWatch]) {
                let event = new CustomEvent('onVar_' + varToWatch + 'Change', {
                    detail: {
                        name: varToWatch,
                        oldValue: localCopyForVars[varToWatch],
                        newValue: window[varToWatch]
                    }
                });
                document.dispatchEvent(event);
                localCopyForVars[varToWatch] = window[varToWatch];
            }
        }
        setTimeout(pollForChange, timeout);
    };
    let respondToNewValue = function (varData) {
        console.log("The value of the variable " + varData.name + " has been Changed from " + varData.oldValue + " to " + varData.newValue + "!!!"); 
    }
    for (let varToWatch of varsToWatch) {
        localCopyForVars[varToWatch] = window[varToWatch];
        document.addEventListener('onVar_' + varToWatch + 'Change', function (e) {
            respondToNewValue(e.detail);
        });
    }
    setTimeout(pollForChange, timeout);
}

By calling the Method:

watchVariables(['username', 'userid']);

It will detect the changes to variables username and userid.

This is what I did: Call JSON.stringify twice and compare the two strings...

Drawbacks:

  • You can only know whether the whole object changes
  • You have to detect changes manually
  • You better have only primitive fields in the object(no properties, no functions...)

This is NOT a production ideal answer, but what it is doing is setting an interval in JavaScript for every 100 milliseconds and checking to see if the variable is changed and when it is, it does something (anything intended by the OP) and then clears the interval, so it sort of simulates what the OP is asking.

let myvar = "myvar";

const checkChange = setInterval(() => {
    if (myvar !== "myvar") {
        console.log("My precious var has been changed!");
        clearInterval(checkChange);
    }
}, 100);

Now if myvar gets changed to something else then this program will say "My precious var has been changed!" :)

I searched for JavaScript two-way data binding library and came across this one.

I did not succeed to make it work in DOM to variable direction, but in variable to DOM direction it works and that is what we need here.

I have rewritten it slightly, as the original code is very hard to read (for me). It uses Object.defineProperty, so the second most upvoted answer by Eliot B. at least partially wrong.

<!DOCTYPE html>
<html>
  <head>
    <title>TODO supply a title</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script>
        const dataBind = (function () {
            const getElementValue = function (selector) {
                let element = document.querySelectorAll(selector)[0];
                return 'value' in element ? element.value : element.innerHTML;
            };
            const setElementValue = function (selector, newValue) {
                let elementArray = document.querySelectorAll(selector);
                for (let i = 0; i < elementArray.length; i++) {
                    let element = elementArray[i];
                    if ('value' in element) {
                        element.value = newValue;
                        if (element.tagName.toLowerCase() === 'select'){
                            let options = element.querySelectorAll('option');
                            for (let option in options){
                                if (option.value === newValue){
                                    option.selected = true;
                                    break;
                                }
                            }
                        }
                    } else {
                        element.innerHTML = newValue;
                    }
                }
            };

            const bindModelToView = function (selector, object, property, enumerable) {
                Object.defineProperty(object, property, {
                    get: function () {
                        return getElementValue(selector);
                    },
                    set: function (newValue) {
                        setElementValue(selector, newValue);
                    },
                    configurable: true,
                    enumerable: (enumerable)
                });
            };
            return {
                bindModelToView
            };
        })();
    </script>
</head>
<body>
    <div style="padding: 20%;">
        <input  type="text" id="text" style="width: 40px;"/>
    </div>
    <script>
        let x = {a: 1, b: 2};
        dataBind.bindModelToView('#text', x, 'a'); //data to dom

        setInterval(function () {
             x.a++;
        }, 1000);
   </script> 
</body>

</html>

JSFiddle.

JSFiddle with original code.

In the provided example a property of object x updated by the setInterval and value of text input automatically updated as well. If it is not enough and event is what you looking for, you can add onchange listener to the above input. Input also can be made hidden if needed.

This is an old thread but I stumbled onto second highest answer (custom listeners) while looking for a solution using Angular. While the solution works, angular has a better built in way to resolve this using @Output and event emitters. Going off of the example in custom listener answer:

ChildComponent.html

<button (click)="increment(1)">Increment</button>

ChildComponent.ts

import {EventEmitter, Output } from '@angular/core';

@Output() myEmitter: EventEmitter<number> = new EventEmitter<number>();

private myValue: number = 0;

public increment(n: number){
  this.myValue += n;

  // Send a change event to the emitter
  this.myEmitter.emit(this.myValue);
}

ParentComponent.html

<child-component (myEmitter)="monitorChanges($event)"></child-component>
<br/>
<label>{{n}}</label>

ParentComponent.ts

public n: number = 0;

public monitorChanges(n: number){
  this.n = n;
  console.log(n);
}

This will now update non parent each time the child button is clicked. Working stackblitz

I came here looking for same answer for node js. So here it is

const events = require('events');
const eventEmitter = new events.EventEmitter();

// Createing state to watch and trigger on change
let x = 10 // x is being watched for changes in do while loops below

do {
    eventEmitter.emit('back to normal');
}
while (x !== 10);

do {
    eventEmitter.emit('something changed');
}
while (x === 10);

What I am doing is setting some event emitters when values are changed and using do while loops to detect it.

Related