0%

Old-fashion Declarative Rendering

In VueJS, most of time if we want to render a component based on user interaction or certain events, we will do it in a declarative way by using v-if directive:

<parent>
<button @click="showDialog = true">Click to show a dialog</button>
<dialog v-if="showDialog" />
</parent>

Now let’s make it a bit complex. We will call some API when user click this button. When such API failed, we will show the dialog component which has two buttons, cancel and retry, both of which we need to setup an event listener for user interaction. Then the code become:

<parent>
<button @click="onClick">Click to show a dialog</button>
<dialog v-if="showDialog" @confirm="doRetry" @cancel="doCancel" />
</parent>
<script>
export default {
methods: {
onClick ()=>{
doSomeAPICall().catch(()=>this.showDialog =true)
},
doRetry ()=>{doSomeAPICall()},
doCancel ()=>{}
}
}
</script>

Issues

Though above code works perfectly, it can be very cumbersome especially when dialog component is being used in a lot of places. So how can we improve this?

One solution actually lies in how Unit Testing tools work like Mount API in Vue Test Utils. So we can actually do this

doSomeAPICall().then(()=>
mount(successDialog)
).catch(()=>
mount(errorDialog)
)

Vue.extend and the Constructor

Here is how to do it. We can import the Vue Component and use it in this way:

import Dialog from './Dialog.vue'
let DialogConstructor = Vue.extend(Dialog)

doSomeAPICall().catch(()=>{
let dialog = new DialogConstructor
dialog.$mount()
document.body.appendChild(dialog.$el) //you can append it anyway you like
})

Apparently there are certain advantages over this approach:

  1. No more looking back to template for logic reference. Everything reside in the API promise callback.

  2. The entire rendering process can be further encapsulated into function for code reuse, e.g you can wrap the cancel/retry into promise call with its own then/catch.

  3. You can mount the dialog into everywhere you want instead of parent component. (Though you can also do it inside the component via el option)

A bit more digging into this constructor:

How to pass props to the component:

let dialog = new DialogConstructor({
propsData: {
propA: 'foo',
propB: 'bar'
}
})

How to listen to event from dialog component:

dialog.$on('confirm', ()=>{ doSomething() })

You can even manipulate child component data directly, though it is not recommended since it can make code hard to read and debug:

dialog.dataA = 'foo'

Conclusion

Vue.extend API can return an constructor so that rendering a component entirely in JS code can be possible. It can be used in frequently used component for better code reuse.

VueJS is using similar structure like EventEmitter for child-to-parent communication with its $emit API. When parent component wants to know certain lifecycle events that child component is in, we can simply trigger an event in related hook:


const Child = {
mounted () {
this.$emit('child-mounted')
}
}

<parent @child-mounted="doSomething"><child /></parent>

With that callback, we can do numerous things like managing auto-focus behavior or getting child component layout sizing (width, height etc.).

However, if the child component is from 3rd party library which does not provide such callback, we can still tap into its lifecycle in this way:

<parent @hook:mounted="doSomething">
<child />
</parent>

Strangely, this is not listed in any of the official document. If you are interested, check the source code

So literally it is just syntax-sugar and will trigger event in this special hook: namespace.

In every Javascript 101 class, closure is always an indispensable part. I wonder how many beginners will stumble into this tiny piece of code:

for (var i > 0; i < array.length; i++) {
button.onclick = function(){alert(i)}
}

But we are not revisiting this intrigue yet important concept in Javascript today. In terms of closure in this article, we are referring the way we store a local variable in our components for later use, like registering a resize function and call removeEventListener for cleanup before component being destroyed.

In VueJS or React class-based component, this is easy like a breeze, since we always have this reference in component, which can be accessed in all lifecycle events like mounted/beforeDestroy (VueJS) and componentWillMount/componentWillUnmount (React)


export default {
mounted() {
this.resizeFn = ()=>{
console.log(this.$el.clientWidth)
}
window.addEventListner('resize', this.resizeFn)
},
beforeDestroy(){
window.removeEventListener('resize', this.resizeFn)
}
}

But in functional programming like React Hooks, we don’t have this and how can we achieve the same thing?

Let’s say we have a demo which show an input. Upon rendering, user has 30 seconds countdown before input changes to disabled status. Any typing will resolve the countdown.

For this, you might instinctively say, let’s just name a variable inside function body and see how it goes:

export default function App() {
let [text, setText] = useState('')
let [disabled, setDisabled] = useState(false)
let timeout;
console.log(timeout)
let onChange = function (event) {
clearTimeout(timeout)
setText(event.target.value)
}
if (timeout) {
timeout = setTimeout(()=>{
console.log('time is over')
setDisabled(true);
}, 30 * 1000)
}
return (
<div className="App">
<h1>{text}</h1>
<input onChange={onChange} disabled={disabled} value={text}/>
</div>
);
}

It looks good but actually it won’t work. The problem is that when user type anything, the entire function will re-run and timeout will be a brand new variable again with initial value of undefined. So this line will never run:

if (timeout) { doSomething() }

Here comes a special hook called useRef. In the very first part of this series we have already introduced this hook for accessing DOM element inside component. But this can also be used for closure purpose. Here is the explanation from official doc:

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

Here is the correct demo which utilize useRef for store the timeout variable

In both VueJS & React world, declarative programming is preferable way for managing parent-child communication and interaction against imperative programming.

Image we have a form, with an input and a submit button. Button is a child component which will be in disabled status unless input is filled in with an email address. So in declarative way, we will pass a prop to button like:


<form>
<input type="email" />
<Button disabled="disabled">Submit</Button>
</form>

In the form component, it does not care how children are implementing disable/enable. It only declares the status that button component should be in.

On contrary to this approach, comes imperative programming, which will specifically calling children’s API being exposed to its parent:


if (isEmailValid) {
button.setEnable(true)
} else {
button.setEnable(false)
}

Above being said, both framework allows their users to calling sub components in imperative way. One reason is that some 3rd party libraries will expose APIs which components need to call imperatively.

VueJS


<Parent>
<Child ref="button" />
</Parent>

It provides $refs to both DOM and child components so that parent can access directly by call:


this.$refs.button.setEnable()

It even provides two special internal properties $parent and $children to refer to the parent and children components respectively. However this needs to be used with precautions as VueJS does not guarantee rendering order in children so if you write some code like


this.$children[0].setEnable

It will be likely to fail sometime later and it is difficult to debug.

React Hooks

For React Hooks, it needs to use a built-in hooks called useImperativeHandle:


const Button = forwardRef((props, ref) => {
let [disabled, setDisabled] = useState(true)
useImperativeHandle(ref, () => ({
setEnable(flag) {
setDisabled(!flag)
}
}));
return <button disabled={disabled}>Hi</h1>;
});
const Parent = () => {
const buttonRef = useRef();
return (
<form>
<input type="email" onChange={buttonRef.current.setEnable()}/>
<Button ref={buttonRef} />
</form>
);
};

In combination with useRef API we introduced before (which can also be referred to both component and DOM element), we can also call child component methods in imperative way

At first glance, it might look like React is not intuitive against VueJS in this specific design. But React can designate which APIs to be exposed to parent instead of exposing entire child component instance. So to use imperative programming in VueJS way, it’s up to developer’s responsibility to make sure it is safe, reasonable and will not mess up child component internal state and model.

What is Computation?

Computation is a technique we often used in programming to save the computed results of expensive function call, which usually depends on input values. Unless those input values changed, the computed result will stay in the memory unchanged, thus optimizing performance by saving from unless re-computation and re-rendering of UI. That’s why it is called as Memorization.

A very basic example is to suppose we have a user list and search box. We can add some computation field called displayUsers to calculate based on the user list, which usually hold the complete data you pull from data backend, against whatever keyword use types in search box.

VueJS: Computed Property

VueJS offer computed property as a part of its class structure.

Remember, the input value in computed property should be reactive so that VueJS can automatically add watcher and fire the computing functions as soon as any of the dependences change.

React Hooks: useMemo

React offer useMemo hooks to do such computation

Comparison

VueJS computed property is more intuitive: value B is based on value A with some functions. But it is inexplicit in its dependence. You have to look at the call for this.xxxx within function body.

On the other hand, React requires user to declare dependence in their useMemo hook. And since you have to pass state or prop into the dependence, it is unlikely to meet recursive problem.