Skip to main content

Reactivity

Reactivity is also a core concept in Leafjs. Just like many other frameworks, Leafjs's reactivity system allows two-way binding and automatic component rerender.

What is reactivity

Reactivity, just like its name, defines a reactive variable that whenever its content changes, everything that uses this variable (either directly or indirectly), will trigger an update.

Assume the following script:

// name's initial value is John
name = 'John';

console.log('Name:', name);

name = 'Doe';

Apparently this script will only log once as a normal variable. But in component rendering, we certainly want to update component content along with its states. So in that case, this script should log twice - one John and one Doe.

Now we can introduce our reactive variable:

name = makeReactive('John');

watch(() => {
console.log(name);
});

name = 'Doe';

Here our makeReactive() function will turn a variable into a reactive variable and watch() will be fired when the reactive variable changes. So the pseudo script output should now be John and Doe this time.

Reactivity in components

The above is the brief concept about reactivity. However, things are a little bit more complicated in actual development. But let us start from the easiest ones. Assume you have the following component:

class Counter extends LeafComponent {
constructor() {
super();
}

render() {
return (
<div>
<button>+</button>
<p>Counter: 0</p>
<button>-</button>
</div>
);
}
}

Currently this component will not do anything when you click on + or -. Now we need to make it update based on user's clicks. In Leafjs, each component has its internal state accessible through this.state. Let's define our current count there:

class Counter extends LeafComponent {
constructor() {
super();

this.state = {
count: 0,
};
}

render() {
return (
<div>
<button>+</button>
{/* state is accessible like a normal variable */}
<p>Counter: {this.state.count}</p>
<button>-</button>
</div>
);
}
}
caution

Never define your state using class property assignment, such as:

class Counter extends LeafComponent {
// DON'T!!
state = {
count: 0,
};

constructor() {
super();
}
}

This will override Leafjs's getter and setter for the state property and making it not reactive.

And now we should add some event listeners to the buttons:

class Counter extends LeafComponent {
constructor() {
super();

this.state = {
count: 0,
};
}

render() {
return (
<div>
<button onClick={() => this.state.count++}>+</button>
{/* state is accessible like a normal variable */}
<p>Counter: {this.state.count}</p>
<button onClick={() => this.state.count--}>-</button>
</div>
);
}
}

Here we can add event listeners with the onClick attribute. Leaf will automatically scan through each attribute and if one starts with on, it will be registered as a even listener. This behavior is the same for custom components.

Thanks to the new ES6 Proxies, Leafjs can implement this amazing native-like reactivity system. If you have used Vuejs before, you might notice that Leaf's reactivity system is very similar to Vue's: that's correct! Leaf's reactivity system was highly inspired by Vuejs's implementation.

note

Because of the way Leafjs handles states, you must set state to an object.

Two-way binding

Sometimes, only changing the component's state according to an event is not enough. For example, we may want to have an input element that changes its content based on both the actual state content change or by user input. This is called a two-way data binding. You can implement this simply by having both a value attribute that points to the state and an event listener that changes the state based on input.

class MyComponent extends LeafComponent {
constructor() {
super();

this.state = {
value: '',
};
}

render() {
return (
<div>
<input value={this.state.value} onChange={(e) => (this.state.value = e.target.value)} />
<p>Input: {this.state.value}</p>
<button onClick={() => (this.state.value = Math.random().toString())}>Random</button>
</div>
);
}
}