What is Reactivity? Reactivity is the ability of a web framework to update your view whenever the application state has changed. It is the core of any modern web framework. To understand what reactivity is, let’s look at an example counter app. This is how you would write in plain JavaScript: const root = document . getElementById ( 'app' ) ; root . innerHTML = ` <button>-</button> <span>0</span> <button>+</button> ` ; const [ decrementBtn , incrementBtn ] = root . querySelectorAll ( 'button' ) ; const span = root . querySelector ( 'span' ) ; let count = 0 ; decrementBtn . addEventListener ( 'click' , ( ) => { count -- ; span . innerText = count ; } ) ; incrementBtn . addEventListener ( 'click' , ( ) => { count ++ ; span . innerText = count ; } ) ; This is how you would do it in Vue: < template > < div > < button v-on: click = " counter -= 1 " > - </ button > < span > {{ counter }} </ span > < button v-on: click = " counter += 1 " > + </ button > </ div > </ template > < script > export default { data ( ) { return { counter : 0 , } ; } , } ; </ script > … and this in React: function App ( ) { const [ counter , setCounter ] = React . useState ( 0 ) ; return ( < > < button onClick = { ( ) => setCounter ( counter => counter - 1 ) } > - < / button > < span > { counter } < / span > < button onClick = { ( ) => setCounter ( counter => counter + 1 ) } > + < / button > < / > ) ; } Notice that with a web framework, your code focus more on updating the application state based on business requirements and describing how our view looks like using templating language or JSX expression . The framework will bridge the application state and the view, updating the view whenever the application state changes. No more pesky DOM manipulation statements ( span.innerText = counter ) sprinkled alongside with state update statements ( counter ++; ). No more elusive bugs of unsynchronized view and application state, when one forgets to update the view when updating the application state. All these problems are now past tense when web frameworks now ship in reactivity by default, always making sure that the view is up to date of the application state changes. So the main idea we are going to discuss next is, How do web frameworks achieve reactivity?

The WHEN and the WHAT To achieve reactivity, the framework has to answer 2 questions When does the application state change?

What has the application state changed? The WHEN answers when the framework needs to start to do its job on updating the view. Knowing the WHAT, allows the framework to optimise it's work, only update part of the view that has changed. We are going to discuss different strategies to determine the WHEN and the WHAT, along with code snippets for each strategy. You could combine different strategies to determine the WHEN and the WHAT, yet certain combinations may remind you of some of the popular web frameworks.

the WHEN The WHEN notifies the framework that the application state has changed, so that the framework knows that it needs to do its job to update the view. Different frameworks employ different strategies to detect when the application state has changed, but in essence, it usually boils down to calling a scheduleUpdate() in the framework. scheduleUpdate is usually a debounced update function of the framework. Because changes in the application state may cause derived state changes, or the framework user may change different parts of the application state consecutively. If the framework updates the view on every state change, it may change the view too frequent, which may be inefficient, or it may have an inconsistent view (may result in tearing). Imagine this contrived React example: function Todos ( ) { const [ todos , setTodos ] = useState ( [ ] ) ; const [ totalTodos , setTotalTodos ] = useState ( 0 ) ; const onAddTodo = todo => { setTodos ( todos => [ ... todos , todo ] ) ; setTotalTodos ( totalTodos => totalTodos + 1 ) ; } ; } If the framework synchronously updates the todos in the view then updates the total todos count, it may have a split second where the todos and the count go out of sync. (Although it may seem impossible even in this contrived example, but you get the point. ) By the way, you should not set totalTodos this way, you should derived it from todos.length , see "Don't Sync State. Derive it!" by Kent C. Dodds. So how do you know when the application state has changed?

Mutation Tracking So we want to know when the application state has changed? Let’s track it! First of all, why is it called mutation tracking? That’s because we can only track mutation. By the word mutation, it infers that our application state has to be an object, because you can’t mutate a primitive. Primitives like numbers, string, boolean, are passed by value into a function. So, if you reassign the primitive to another value, the reassignment will never be able to be observed within the function: let data = 1 ; render ( data ) ; data = 2 ; function render ( data ) { setInterval ( ( ) => { console . log ( data ) ; } , 1000 ) ; } Object on the other hand, is passed by reference. So any changes to the same object can be observed from within: let data = { foo : 1 } ; render ( data ) ; setTimeout ( ( ) => { data . foo = 2 ; } , 1000 ) ; function render ( data ) { setInterval ( ( ) => { console . log ( data . foo ) ; } , 1000 ) ; } This is also why most frameworks’ application state is accessed via this , because this is an object, changes to this.appState can be observed / tracked by the framework. Now we understand why is it called mutation tracking, let’s take a look at how mutation tracking is implemented. We are going to look at the two common types of object in JavaScript, the plain object and the array. (Though if you typeof for both object or array, they are both "object" ). With the introduction of ES6 Proxy, the mutation tracking method has become much straightforward. But still, let’s take a look at how you can implement a mutation tracking with / without ES6 Proxy.

Prior Proxy To track mutation without proxy, we can define a custom getters and setters for all the property of the object. So whenever the framework user changes the value of a property, the custom setter will be called, and we will know that something has changed: function getTrackableObject ( obj ) { if ( obj [ Symbol . for ( 'isTracked' ) ] ) return obj ; const tracked = Array . isArray ( obj ) ? [ ] : { } ; for ( const key in obj ) { Object . defineProperty ( tracked , key , { configurable : true , enumerable : true , get ( ) { return obj [ key ] ; } , set ( value ) { if ( typeof value === 'object' ) { value = getTrackableObject ( value ) ; } obj [ key ] = value ; console . log ( ` ' ${ key } ' has changed. ` ) ; } , } ) ; } Object . defineProperty ( tracked , Symbol . for ( 'isTracked' ) , { configurable : false , enumerable : false , value : true , } ) ; return tracked ; } const appState = getTrackableObject ( { foo : 1 } ) ; appState . foo = 3 ; Inspired by Vue.js 2.0’s observer. However, you may notice that if we are defining getters and setters on the existing properties of the object, we may miss out changes via adding or deleting property from the object. This is something you can’t fix without a better JavaScript API, so a probable workaround for this caveat is to provide a helper function instead. For example, in Vue, you need to use the helper function Vue.set(object, propertyName, value) instead of object[propertyName] = value . Tracking mutation of an array is similar to mutation tracking for an object. However, besides being able to change the array item through assignment, it is possible to mutate an array through its mutating method, eg: push , pop , splice , unshift , shift , sort and reverse . To track changes made by these methods, you have to patch them: const TrackableArrayProto = Object . create ( Array . prototype ) ; for ( const method of [ 'push' , 'pop' , 'splice' , 'unshift' , 'shift' , 'sort' , 'reverse' , ] ) { const original = Array . prototype [ method ] ; TrackableArrayProto [ method ] = function ( ) { const result = original . apply ( this , arguments ) ; console . log ( ` ' ${ method } ' was called ` ) ; if ( method === 'push' || method === 'unshift' || method === 'splice' ) { } return result ; } ; } function getTrackableArray ( arr ) { const trackedArray = getTrackableObject ( arr ) ; trackedArray . __proto__ = TrackableArrayProto ; return trackedArray ; } const appState = getTrackableArray ( [ 1 , 2 , 3 ] ) ; appState . push ( 4 ) ; appState [ 0 ] = 'foo' ; Inspired by Vue.js 2.0’s array observer. CodeSandbox for mutation tracking of object and array In summary, to track mutation on an object or array without Proxy, you need to define custom getters/setters for all properties, so that you can capture when the property is being set. Besides that, you need to patch all the mutating methods as well, because that will mutate your object without triggering the custom setter. Yet, there’s still edge cases that cannot be covered, such as adding new property or deleting property. There’s where ES6 Proxy comes to help.

With Proxy Proxy allow us to define custom behaviours on fundamental operations on the target object. This is great for mutation tracking, because Proxy allow us to intercept setting and deleting property, irrelevant to whether we uses index assignment, obj[key] = value or mutating methods, obj.push(value) : function getTrackableObject ( obj ) { for ( const key in obj ) { if ( typeof obj [ key ] === 'object' ) { obj [ key ] = getTrackableObject ( obj [ key ] ) ; } } return new Proxy ( obj , { set : function ( target , key , value ) { console . log ( ` ' ${ key } ' has changed ` ) ; if ( typeof value === 'object' ) { value = getTrackableObject ( value ) ; } return ( target [ key ] = value ) ; } , deleteProperty : function ( target , key ) { console . log ( ` ' ${ key } ' was deleted ` ) ; return delete target [ key ] ; } , } ) ; } const appState = getTrackableObject ( { foo : 1 , bar : [ 2 , 3 ] } ) ; appState . foo = 3 ; appState . bar . push ( 4 ) ; appState . bar [ 0 ] = 'foo' ; So how do we use mutation tracking? The good thing about mutation tracking is that, if you noticed in the example above, the framework user is unaware of the tracking and treats appState as a normal object: appState . foo = 3 ; appState . bar . push ( 4 ) ; appState . bar [ 0 ] = 'foo' ; We can set up the tracking during the initialisation of the component, either: track a property of the component,

track the component instance itself,

or something in between the above class Component { constructor ( initialState ) { this . state = getTrackableObject ( initialState ) ; } } class UserComponent extends Component { constructor ( ) { super ( { foo : 1 } ) ; } someHandler ( ) { this . state . foo = 2 ; this . other . foo = 2 ; } } class Component { constructor ( ) { return getTrackableObject ( this ) ; } } class UserComponent extends Component { constructor ( ) { super ( ) ; } someHandler ( ) { this . foo = 1 ; } } Once you’ve able to track application state changes, the next thing to do is to call scheduleUpdate instead of console.log . You may concern whether all these complexities is worth the effort. Or you may be worried that Proxy is not supported to older browsers. Your concern is not entirely baseless. Not all frameworks use mutation tracking.

Some frameworks design their API in the way such that it “tricks” the framework user to tell the framework that the application state has changed. Instead of remembering to call scheduleUpdate whenever you change the application state, the framework forces you to use their API to change application state: this . appState . one = '1' ; scheduleUpdate ( ) ; this . setAppState ( { one : '1' } ) ; This gives us a much simpler design and less edge case to handle: class Component { setAppState ( appState ) { this . appState = appState ; scheduleUpdate ( ) ; } } Inspired by React’s setState . However, this may trip new developers into the framework: class MyComponent extends Component { someHandler ( ) { this . appState . one = '1' ; } } ... and it maybe a bit clumsy when adding / removing items from an array: class MyComponent extends Component { someHandler ( ) { this . appState . list . push ( 'one' ) ; this . setAppState ( { list : this . appState . list } ) ; this . setAppState ( { list : [ ... this . appState . list , 'one' ] } ) ; } } A different approach that may have the best of both world is to insert scheduleUpdate in scenarios that you think that changes may most likely happen: Event handlers

Timeout (eg: setTimeout , setInterval , ...)

, , ...) API handling, promises handling

... So, instead of enforcing framework users to use setAppState() , framework users should use the custom timeouts, api handlers, ...: function timeout ( fn , delay ) { setTimeout ( ( ) => { fn ( ) ; scheduleUpdate ( ) ; } , delay ) ; } import { $timeout } from 'my-custom-framework' ; class UserComponent extends Component { someHandler ( ) { $timeout ( ( ) => { this . appState . one = '1' ; } , 1000 ) ; setTimeout ( ( ) => { this . appState . two = '2' ; } , 1000 ) ; } } Inspired by AngularJS’s \$timeout Your framework user can now be free to change the application state the way he wants, as long as the changes are done within your custom handlers. Because at the end of the handler, you will call scheduleUpdate() . Similarly, this may trip new developers into the framework too! Try search "AngularJS $timeout vs window.setTimeout" You may think, what if there are no state changes in the handler function, wouldn’t calling an extra scheduleUpdate() be inefficient? Well so far, we haven’t discussed what’s happening in scheduleUpdate() , we can check what has changed (which will be covered in the next section), and if there’s nothing change, we can skip the subsequent steps. If you look at the strategies that we have tried so far, you may have noticed a common struggle: allow framework user to change the application state in any way he wants

achieve reactivity without much runtime complexity. At this point, you got to agree that enforcing framework developers to call setAppState whenever they want to change the application state, requires less runtime complexity from the framework, and it’s unlikely to have any corner cases or caveats that need to handle. If the dilemma is between developer expressiveness versus runtime complexity, probably we could get the best of both worlds by shifting the complexity from runtime to build time?

Static analysis If we have a compiler that allow framework users to write: class UserComponent { someHandler ( ) { this . appState . one = '1' ; } } and compiles it to: class UserComponent { someHandler ( ) { this . appState . one = '1' ; scheduleUpdate ( ) ; } } Then, we would really have best of both worlds! 😎 Let’s look at different scenarios that the framework user would write, and see whether we know when to insert the scheduleUpdate() : class UserComponent { someHandler ( ) { this . appState . one = '1' ; this . foo = 'bar' ; const foo = this . appState ; foo . one = '1' ; doSomethingMutable ( this . appState ) ; function doSomethingMutable ( foo ) { foo . one = '1' ; } this . appState . obj = { data : 1 , increment ( ) { this . data = this . data + 1 ; } , } ; this . appState . obj . increment ( ) ; this . appState . data . push ( '1' ) ; this . appState . list = { push ( item ) { console . log ( 'nothing change' ) ; } , } ; this . appState . list . push ( '1' ) ; } } Allow me to summarise some complexities faced in the example above: It is easy to track direct changes to the application state, but it is extremely difficult to track changes made indirectly, eg: foo.one , doSomethingMutable(this.appState) or this.appState.obj.increment()

, or It is easy to track changes through assignment statements, but extremely difficult to track changes made through mutating methods, eg: this.appState.list.push('1') , I mean how do you know the method is mutating? So, for Svelte, one of the frameworks that use static analysis to achieve reactivity, it only ensures reactivity through assignment operators (eg: = , += , …) and unary arithmetic operators (eg: ++ and -- ). I believe that there’s room yet to be explored in this space, especially at the rise of TypeScript, we may be able to understand our application state better through static types.