0%

In React or VueJS, we often author components that are considered as layout components, which sets the generic layout UI like header/sidebar/footer etc so that they can be reused in different places. Each time a layout component is called, it does not care about what to be inserted into its children. In VueJS, we have Slots API and in React we have props.children to achieve that.

However, in certain scenarios we want to pass some data from parent into its children, even the parent does not know what kind of component will be of its children, given the fact that it is a generic layout component. How can we do so?

Let’s make a demo: The parent component will start counting how many seconds you have stayed in this web page (yep, that’s exactly what you saw in reactjs.org) and child component will consume this count and render it.

VueJS

We will resort what is referred as Scoped Slots


let TimeOnPage = {
template: `<div>
<slot v-bind:second="second" />
</div>`
data () {
return {second: 0}
},
mounted () {
setInterval(()=>this.second++, 1000)
}
}

let Child = {
template: `<time-on-page>
<template v-slot="slotProps">
{{slotProps.second}}
</template>
</time-on-page>`,
components: {TimeOnPage}
}

In the parent TimeOnPage we can use v-bind to bind an object to slot with structure like

slotProps = {
second: 0 //0 is reactive in child component this.second
}

so in child components can access this object. It is a common practice to name this object to slotProps in child v-slot directive but you can actually name it to whatever you would like.

React Hooks

With React Hooks we can easily share state between components by using customized hooks:

const useTimeOnPage = () => {
const [second, setSecond] = useState(0);
useEffect(() => {
setTimeout(() => {
setSecond(second + 1);
}, 1000);
}, [second]);
return second;
}
const Demo = () => {
const second = useTimeOnPage();
return <div>second</div>
}

However this is not exactly same as our topic, since we want to keep the data in the parent component and share in its immediate children. Here we can use a technique that is often referred as Render Props:

const Child = (props) => {
return <div>{props.second}s</div>;
};

const TimeOnPage = (props) => {
const [second, setSecond] = useState(0);
useEffect(() => {
setTimeout(() => {
setSecond(second + 1);
}, 1000);
}, [second]);
return <div>{props.render(second)}</div>;
};

<TimeOnPage render={(second) => <Child second={second} />

In this example, TimeOnPage will accept a function as props (The prop name can be called anything but usually we will name it as render) and pass its data as function arguments. The function will return a component which can access these arguments in this closure.

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.