Leveraging Render Props in Vue
Code sharing between your Vue components is an important concept to consider when designing and developing your components. Render props, a technique being largely adopted by the React community, helps to solve this issue. So what are render props?
“The term “render prop” refers to a simple technique for sharing code between React components using a prop whose value is a function.”
React Documentation
So essentially, a component that is utilizing a render prop takes a function which will return an element (this could also be another Vue component) and uses that function to render the view rather than defining your own template.
What does that actually look like? Let’s start small and work our way up.
We’ll create a component, Greeter, which will take in a function as a prop to execute. It will expose a method, greet
, which will just pop open an alert window.
Let’s break this down really quick. First we are defining the props we will be receiving. We can see we are expecting a prop named render
of type Function
and by default it is just a function that returns nothing. Next, we define our greet
method which takes no parameters and alerts a simple message. Lastly, we define a render
method to tell the component how to actually render. Let’s look into this in more detail.
Vue Render Methods
If you already know about a Vue component’s render
method go ahead and skip this section.
Typically in Vue components you will see the markup for them defined within <template>
tags in the case of Single File components. Alternatively, you may see the template
property on the Vue instance itself or the el
property of where to mount that component. But there is another option to define your component’s templates and that is the render
method.
This render
method allows you to tap into the raw JavaScript that powers template rendering behind the scenes. This render method receives a function as a parameter called createElement
. If you do come across these in the real world you will probably see this function aliased to just h
. And if you are asking why h
like I did, here is what Evan You had to say:
“It comes from the term “hyperscript”, which is commonly used in many virtual-dom implementations. “Hyperscript” itself stands for “script that generates HTML structures” because HTML is the acronym for “hyper-text markup language”.”
This createElement
function, which I’ll now be just calling h
as well, takes in three parameters. The first is the element to render. This could be ’div’
, ‘ul’
, ‘p’
, or event a component like MyComponent
. The second is an object which would be equivalent to the attributes you would pass on that element within a template. Finally, the third parameter is a string or an array of VNodes to render within that element. An entire blog post can be dedicated to the render
function alone so refer to the Vue guides for more information.
As a quick example if we wanted to create a div
that had a class name container
with the text Hello World
in it, it would look something like this:
Which would result in the markup:
<div class=”container”>
Hello World
</div>
Writing Your Render Prop
We have our Greeter
component setup, let’s actually use this thing. We’ll define our application root.
const app = new Vue({
el: ‘#app’
})
And on this we will declare a render
method which will pass a render prop to our Greeter
component.
This is kinda low level stuff…at least compared to declaring regular HTML within template
tags or even writing some JSX. So let’s go through it line by line.
We declare our render
method and get our h
(createElement
) function as a parameter. First we create a <div>
with no attributes but it holds our Greeter
component. Our Greeter
component does take an attribute though! This is the component that will be receiving the “render prop”.
On our object we’ll define a key props
which you may have guessed are the props we are passing to that component. So we define our render
function here. Remember above in our Greeter
component’s render
we did this:
render () {
return this.$props.render({
greet: this.greet
})
}
Within our render
we defined an object with a key of greet
. We are destructuring it in our parameters here. This render function, which remember is a prop of our Greeter
component, returns a <button>
element. And we can see this button has a click
attribute which when clicked will execute the greet
function we destructured above. And this greet
function actually refers to the greet
method we defined on our Greeter
component.
Completed Example:
Spicing Things Up
Let’s boost the complexity up a bit more just to get used to the idea of render
functions and passing things into render props. We’ll keep our Greeter
component but we’ll introduce a new component People
which will just render a list of people from an array. We’ll also add some new functionality, our alert will alert the person’s name we select and it will provide some contextual background-color so we know who was selected last.
First we’ll define the necessary state and methods to our Greeter
component.
Our state, selected
, just defaults to the first element of an array. We then added a select
method which takes an array index and just updates our component’s state to that index. And lastly, we expose these new properties within our render
.
Next, we will begin to stub out our root component. Let’s create some local state on our data
called people
which will initialize to an array of three names. And we will start building out our render
method, starting off the way we did in our previous example. This time within our render prop we will extract the new data and methods exposed in our Greeter
component. And for now, the render prop will just return null
. Let’s get that People
component started!
Our People
component will be what our Greeter
component will be passing its methods and data to. And just to reiterate, above we said this component will take in an array of names (just strings) and print them out within a <ul>
.
Again, this looks a little obtuse. Especially if you are not used to render methods but I really want to expose how your usual ways of defining templates are being interpreted behind the scenes.
You’ll notice some things different about this component. For one, we defined this component as functional
which just means it is stateless. Secondly, you might notice that it is accepting a prop of people
which is an array but we are not defining any props on our component. In Vue 2.3.0+ we can omit this on functional components. Finally, you may notice a second parameter we did not talk about before being passed to our h
function.
Within functional components we do not have access to this
. So we get a context
parameter and one of the things this context
object contains is our props. You can read more about functional components and the context
object on the Vue guide.
Our component is then creating a <ul>
tag with no attributes. But as child nodes we our iterating over our people
array passed in as a prop using Array.prototype.map
method. And within our map we are just returning <li>
elements. These <li>
elements have an on click action, which on click takes a function off our props called select
and passes in the current index
of the array. It also takes another function called greet
, which we saw in the previous example, and passes in the current person’s name. Furthermore, we see another class attribute which checks a selected
property on our props and see if it matches the current array index. If it does, we throw a selected
class onto that <li>
element.
.selected {
background-color: pink;
}
Finally, as the text node for the <li>
element we are just passing in the person’s name. If that’s still a little confusing, here is what it would look like within a template tag:
With that created, let us revisit our root instance component and finish our render prop.
Just to really drive it home, this is all our render prop is:
Let’s revisit what we said a “render prop” was in the beginning. It is a function which will return an element (this could also be another Vue component) and uses that function to render the view rather than defining your own template.
We define our prop render
, which is a function. It returns our People
component. Our Greeter
component never actually defines any HTML to render, all it does is expose state and methods which is used by our People
component.
Completed Example:
The Big Picture
We just went through two pretty basic examples and you may still be wondering…what’s the point? The whole appeal of utilizing this pattern is for code reuse.
Vue utilizes a component architecture for developing applications. We define components, how they look, how they behave, and such and we reuse those components throughout our app. But how would you go about sharing similar behaviors between components? Let’s look at some alternative patterns.
Alternative Patterns
Exploring Mixins
Mixins can work in some cases and are perfectly viable solution to the issue of code reuse. But they do come with some issues you should be aware of.
Naming Collisions
Since Mixins are available on this
context of a Vue component we may find a property defined on our Mixins clash with something that is defined on our component. This may not be an issue at first you could just rename it. However, it does become an issue if multiple components depend on a Mixin and renaming a property on a Mixin could cause those components to break…which brings us to
They can introduce “hidden” dependencies
A component’s method, lifecycle hook, or any other property might rely on a function defined within a mixin. A mixin’s function might depend even on another mixin! Removing, renaming, or adding state, methods, and other properties might break mixins.
Hidden Complexity
As Mixins grow they introduce more dependencies your component relies, increase the chance for naming collisions, which makes for a harder to reason about codebase.
No Functional Components
If you wanted to use a method on a mixins for example, your component must have access to this
. Since functional components do not have any access to this
context we can not use functional components with mixins.
Higher Order Components (HOCs)
The React community’s answer to Mixins was HOCs. HOCs at their core are just components that return components. Due to the functional nature of React, composing components through HOCs seemed natural. I never really saw this pattern take off within the Vue community. There was an interesting back and forth between LinusBorg (Vue Core member) and others in this issue.
HOCs were pretty pervasive though in React…that is of course until render props. See this tweet from Michael Jackson:
The Real World
I know what you are probably thinking, ”I’m not touching those render function with that ‘h’ nonsense.” Or possibly thinking of the spawn of Cthulhu that one person at work decides to birth into existence. Trust me, I know. I can already feel half of you shudder at the idea of using JSX in Vue…or in general. It’s okay. You’re in luck. There is a more ‘Vue-like’ way of pulling this pattern off. And it lies within Scoped Slots.
Scoped Slots
If you are already familiar with scoped slots go ahead and skip this section. Otherwise stick around.
Scoped slots were added in 2.1.0 and are a powerful feature. They allow you to expose data from a child component to a parent component. So in your child component you can define your <slot>
tags where you want your slotted content to go. But now, we can use the v-bind
directive to attach any data from within the child component on the <slot>
tag that we want to expose up to the parent component.
We can imagine a UserList
component (the parent) which renders individual user information.
This might not work in all scenarios, what if we wanted the user’s email too? And we wanted that to be in anchor tag with a mailto
href
. Do we create an entirely new component, which would basically do the same thing? There has to be a better way. And there is a better way utilizing scoped slots.
We use the v-bind
shorthand syntax here on our <slot>
and we are binding a user
property to the current user
within our v-for
iteration.
Now we can go to where the component is being used and do this.
We define a local variable called props
and you can name this anything you want its just a variable. This variable, props
, is an object containing whatever data you bound to your <slot>
tag in the component file. And now within those <template>
tags you can freely use whatever data you passed up within your parent component.
Another cool thing about this, we can destructure this object.
Render Props via Scoped Slots
In most cases, we probably want to avoid using the raw render functions presented earlier. There is a lot going on, it is hard to keep track of all the different elements and their attributes. For me, a huge incentive to learn Vue was looking at Single File components and I’m sure that was the case for a lot of you too.
And to be honest, scoped slots are the Vue way of doing render props. Don’t believe me? Evan You even said it in Adam Wathan’s podcast, Full Stack Radio. And I would go ahead when you’re done here to give that a listen!
The awesome thing about render props and scoped slots are they separate behavior from presentation. I have told others that we can expose an interface (not in the OOP sense, think more like an API) that other components can leverage.
Out in Production
At my current job there are certain sections of the application that require us to expose a lot of data to the user. Previously, we just used data tables and called it a day. However, we were tasked with presenting some of these views in a more user-friendly way. And in some cases, the data isn’t tabular and might be more appropriate in a list format. However, we were running into patterns where basic CRUD functionality and other actions such as filtering through the data were common among all of them.
So let’s walk through a similar scenario. We will:
- Create a simple data-table component
- Create a list component
- Extract similar behavior through scoped slot
Data Table
We will start off with our data table component.
Requirements
- Display table of data from an array of objects
- Client-side filtering
- Delete row from table
Let’s keep things basic and make some assumptions about our data.
- We will receive a prop, records
, which is an array of objects containing our tabular data
- We will receive a prop, columns
, which is an array of objects defining our <th>
- We will receive a prop, filter
, which is a string containing a filter query — what keywords to search the table by
So in main parent component we would probably fetch some data through an API or continue passing down a prop to this component. For brevity sake, we’ll just hard code it in our component’s state.
Next, we will stub out our AppTable
component.
This is pretty basic. So far we are just defining our prop definitions. The table is just basic v-for
directives making sure to set a key
on every loop. But notice we are not iterating over a filteredRows
value even though it is not defined anywhere on our component’s JavaScript. Let’s add that now.
We are defining a computed property on our component named filteredRows
which has a dependency on our records
prop and our filter
prop. Vue will automatically detect if either of these changes and reevaluate the computed property for us, otherwise it keeps the data cached.
The filtering itself is happening within our .filter
callback. We are receiving each element within the array here, which we are aliasing to row
. Since each row
is an object we are creating an array of those object keys with Object.keys
and using Array.prototype.some
on that array. The array method some iterates over an array just like map
, reduce
, and filter
does. The callback function tests whether the current element results in a truthy or falsey value based on the expression.
We then test whether the String representation of our data matches the filter query. If it does, we return that object which eventually gets passed into our new array created by .filter
. If it doesn’t match, it gets filtered out.
Back in our parent component let’s register our AppTable
component.
<script>
import { AppTable } from ‘./components’export default {
name: “App”,
components: {
AppTable
},
// ...
</script>
Next, we will define our App’s template to render our table and a simple <input>
which will v-model
the filter
we defined on our component’s local state.
So now that we have the table displaying let’s tackle removing a row. We’ll revisit our <thead>
within our component and add another <th>
for our “remove” action.
Now let’s add a small button to delete the row.
<td>
<button class=”delete” @click=”removeRow(row)”>×</button>
</td>
Finally, add our method to go with our new click handler.
Here we are taking our records
prop and filtering out any object that has the same ID as the row we passed in from our click handler and sending that as a payload on a ‘remove’ event.
Back on our parent component we can listen for our new event like so:
And now we should see the rows being deleted.
List Component
Requirements
- Display list of data from an array of objects
- Client-side filtering
Immediately, you’ll probably notice the requirements are pretty much the exact same. For now we will not be adding or removing items from the list but later we can see how we can bake in that functionality if we wanted to using scoped slots.
This isn’t anything groundbreaking and I’m sure you’ve done something similar a million times before. We are just iterating over an array. You’ll notice however, that we are using the exact same computed
property that we used for our AppTable
component.
Completed Example:
Another thing to note here is unless we update the template and styles we are strictly locked to a basic <ul>
. What if we wanted the same functionality but within a inline list? Or what if we had a list group of contacts that were to be displayed within a custom <card>
component?
We could just create new components and repeat the same logic over and over again. Instead let’s extract this logic out to a new component.
Refactor using Scoped Slots
Taking our <app-list>
component let’s refactor it using scoped slots. And this is going to be really simple. We will start by just iterating over our records
prop.
And we don’t even have to touch our component’s JavaScript. So we are iterating over our filteredList
computed property which is just our filterable records
prop. We then take each item within the records
array and pass that up to the parent component by binding it to the value of item
.
In our parent component we can do something like this:
Play around with it yourself!
Completed Example:
Why is this cool?
For one, our List
component became really flexible. We can render it however we want to. The view that is presented to the user is completely up to us, not the component.
I’m sure you have come across a component library or single component that was a pain to style how you want it. Perhaps it wasn’t accessible to screen readers or the markup used to render wasn’t semantic at all. Sometimes it is just easier to be able to finely tune the component to fit your needs. And that is exactly what we can accomplish with scoped slots.
Let’s continue.
Leveraging the Power of Scoped Slots and Render Props
The central idea behind a “render prop component” is to utilize scoped slots. And that’s it, we won’t be returning any markup at all. Instead, we create a component which will handle all the state and actions for a set of components which share common functionality. So we have our filterable table and list components above. Let’s add another feature to our List
component, the ability delete an item.
First, let’s identify all the common features between these two components.
- Iterate over an array of objects
- Filterable data
- Delete Item
Okay, let’s stub out our CollectionManager
component.
We see that render
method again on our Vue component. And don’t worry, we won’t be messing with any createElement
functions here. The basic idea here is we have a component that will take in the default scoped slot, basically a non-named slot, and invoke it. Currently, ours does nothing other than render the given slot. So how can we add features to this?
Remember that this is still just a Vue component. We still have access to everything a Vue component has access to like methods, computed properties, lifecycle hooks, etc. So let’s define some props.
You should recognize these because we used both of these props in our <app-table>
and <app-list>
components. But this still doesn’t really do anything…
Let’s think back to why you would want to use this pattern. For code reuse between components. And now remember that computed property that literally used the exact same code between components? Let’s put that here instead.
Now to expose this computed property back up the parent we would place it on the <slot>
tag like so:
<slot :items=”items”></slot>
The idea is the same within this render
method. Instead we pass an object to the function which will hold our scoped data.
render () {
return this.$scopedSlots.default({
items: this.items
})
}
Our computed property items are now available on our slot-scope. Let’s see how we actually use this component now. We’ll import it to our App
component, the same place where we are rendering our old Table and List components. Let’s tackle refactoring our <app-table>
.
Let’s update our template to use our <collection-manager>
.
We take our <collection-manager>
component and completely wrap our <app-table>
. Remember that within our <collection-manager>
we are just returning the default scoped slot within our render
. Next we define our slot-scope
and destructure the items
computed property off it. Now within our slot-scope
we have access to it. Instead of just passing records
directly we first pass it through as a prop on our <collection-manager>
because that component uses it to create the computed property, items
. This is the same items
that is currently available as our slot-scope
so then we can pass that to our <app-table>
component.
Furthermore, we can also pass our filter
prop through the <collection-manager>
since it is needed to correctly filter through the records
prop, which eventually is passed to our <app-table>
as items
.
This allows us to refactor our <app-table>
even further. Let’s revisit the component.
Since the <collection-manager>
is handling the filtering for us we can go ahead and remove the filter
prop declaration along with the entire computed
section. Remember that items
is now the same as our previous filteredRows
computed and items
is being passed in as our records
prop now.
That allowed us to remove a lot of code from our component. Let’s see if we can move the item removal here as well.
We will define a method removeItem
which takes an item object and removes it from our array of records
. For it to work, we just need to create it and expose it on our slot-scope object.
And now let’s use this within our <app-table>
.
We’ll update our prop definitions to accept our remove
function from our <collection-manager>
component.
remove: {
type: Function
}
We can also remove our entire methods
section since we will just be utilizing the remove
prop.
Lastly, we’ll update our <template>
to use the remove
function as the click handler for the delete button.
<button class=”delete” @click=”remove(row)”>×</button>
Now in our App
component let’s pass in our remove
function. And remember to listen for the remove
event to update our records
.
One final thing to note, looking at our <app-table>
component we no longer have anything that is stateful. So if we wanted to we could define it as a functional component.
We can now move onto our <app-list>
component. We can wrap our <collection-manager>
around it as well and expose those slot-scoped properties to it. And we really don’t have to do much to make this work for us based off our previous refactor of this component. We will actually be getting rid of a large chunk of it.
We can safely remove the computed
property and the filter
prop from the component definition. Next we’ll just have to update our <template>
to use just records
instead of the previous computed property.
Now in our App
component let’s wrap it within our <collection-manager>
.
We updated our records
prop to take in the items
exposed by our <collection-manager>
Next, we added a button to remove the item from the list. Notice how these are the exact same things we just passed to our <app-table>
component.
Completed Example:
Closing Thoughts
I hope you can notice the power behind this pattern. It allows us to do some really cool things and keep our code DRY between components. I really enjoy how this pattern allows us to create more functional components than compared to another pattern like Mixins.
Is there a time to avoid this pattern?
I would say it depends. If you find yourself repeating code within your components then this may be something to reach for. Otherwise, don’t bother with the added complexity. Keep your components as simple and small as possible.
Render Prop Code Smells
If you do decide to use a render prop it’s sometimes easy to get out of control with it. If you find yourself basically interfacing all of Lodash’s methods through a render prop or exposing a list 100 lines long in your render method you should probably really stop and consider if you should break it out into a different component.
Additional Materials
- React: Render Props — Official React documentation on Render props
- Use a Render Prop! — React article but the concepts carry over
- Renderless Components in VueJS — Adam is doing some really cool stuff, if you’re into Vue you really need to be following him
- Full Stack Radio: Evan You, Advanced Vue Component Design — Evan and Adam talk about render props here
- Introduction to Vue.js Render Functions — More in-depth information on the render functions we utilized in the beginning