Emulate render props in Vuejs
Render props explained
React has a nice pattern to reuse code, it’s called render-props. This pattern allow the developer to elegantly replace the popular High Order Component. We can imagine a component that take a promise as a props and that render a component when the promise is pending, another component when the promise is fulfilled and another when the promise is rejected.
Here is an example with pseudo code and High Order Component:
const AsyncHOC = HOCAsyncConstructor(promise)(onPending, onFulFilled, onRejected);
Now we have a component AsyncHOC
that will render onPending
component when the promise is pending, onFullFilled
when the promise is resolved and onReject
when the promise is rejected.
The problem pointed out by the react community is that the HOC component is hidden. Child component and parent component aren’t aware of the existence of the HOC component. A props collision can happen. if the parent component decide to call the child component with the prop and the HOC also decide to call the component with the same prop, then, the prop given by the parent is lose.
What we learnt from react
React come with a pattern named: Render props
.
With this pattern, we don’t create a hidden component which operate as a proxy. We just tell our component how to render its children:
<Async
promise="promise"
onPending={() => (<div> Pending... </div)}
onFulFilled={(value) => (<div> {value} </div>)}
onRejected={(error) => (<div> {error} </div>)} />
As we can see, all the behavior is explicit. We know what we render on the different state of the promise.
Use this pattern with scoped slots in vue
Since this pattern is easy to produce with react (jsx). It not goes well with vue templates. But we can use scoped slot to emulate the behavior very easily.
First, create our async component.
<template>
<div>
<slot name="onFulFilled" :value="value" v-if="value !== null" />
<slot name="onRejected" :error="error" v-else-if="error !== null" />
<slot name="onPending" v-else /> </div>
</template>
We create a template and we expose 3 named slots:
onFulFilled
used when the promise is resolvedonRejected
used when the promise is rejectedonPending
used when the promise is pending
onFulFilled
is displayed when value
is not null
. Otherwise, if error
is not null
, then, display the slot onRejected
, otherwise, render the onPending
slot.
The script tags
<script>
export default {
props: ['promise'],
mounted() {
this.promise
.then(value => this.value = value)
.catch(error => this.error = error); }, data: () => ({
value: null,
error: null
})
};</script>
Now we can use our component as follow:
<Async :promise=mypromise> <template slot="onFulFilled" slot-scope="props">
<div> Promise is resolved with value: {props.value} </div>
</template> <template slot="onRejected" slot-scope="props">
<div> Promise is rejected with error: {props.error} </div>
</template> <template slot="onPending">
<div> Pending... </div>
</template></Async>
Now, we can reuse our Async component everywhere without hiding what it does.