In this article I am trying to summarize a couple of techniques that are typically used for building reusable components. The idea came to me from a course on VueJS by Michael Thiessen. I am writing the examples in simple javascript functions and not in any framework code.
Video course Reusable components by Michael Thiessen is another take on some basic features of VueJS. Michael is explaining the concepts that one need to create a reusable component and specific VueJS features only happen to be the implementation.
Iβm going to summarize every technique (Michael calls them levels of reusability), but Iβll only use simple javascript functions as examples, since the ideas are not framework dependant.
Templating
Is about reusing the same markup. Copy pasting a piece of markup might be a good starting point. And it might be enoughβnot everyting needs to be absolutely DRY and componentized.
const buttonTemplate = `<button>π </button>`
function MyButtonComponent() {
return `<button>π Foo</button>`
}
MyButtonComponent()
const buttonTemplate = `<button>π </button>`
function MyButtonComponent() {
return `<button>π Foo</button>`
}
MyButtonComponent()
Result
Configuration
Is parametrized template. This is the core of any templating language. Also this is how a so called dumb component looks like.
function MyButtonComponent({ text }) {
return `<button>π <em>${text}</em></button>`
}
MyButtonComponent({ text: "My Emphasised Button" })
function MyButtonComponent({ text }) {
return `<button>π <em>${text}</em></button>`
}
MyButtonComponent({ text: "My Emphasised Button" })
Result
Adaptability
A component is adaptable, when it can be used for different purposes. And it is typically the case when the component accepts slots for external content.
function MyButtonComponent(content) {
return `<button>${content}</button>`
}
MyButtonComponent("<strong>My Strong Button π</strong>")
function MyButtonComponent(content) {
return `<button>${content}</button>`
}
MyButtonComponent("<strong>My Strong Button π</strong>")
Result
Inversion
Only the child component in this example knows a special algorithm to compute something, while only the parent component knows how to present the information in text. Passing a funciton as a component property is known as render props.
// Child component
function MyButtonComponent(contentFunction) {
const computeLevelFromContent = (content) => content.length
const level = computeLevelFromContent(contentFunction(""))
const content = contentFunction(level)
return `<button>${content}</button>`
}
// Parent component
function Parent() {
return MyButtonComponent((level) => `My Powerful Button (level: ${level})`)
}
Parent()
// Child component
function MyButtonComponent(contentFunction) {
const computeLevelFromContent = (content) => content.length
const level = computeLevelFromContent(contentFunction(""))
const content = contentFunction(level)
return `<button>${content}</button>`
}
// Parent component
function Parent() {
return MyButtonComponent((level) => `My Powerful Button (level: ${level})`)
}
Parent()
Result
Extension
Extensible component is like an adaptable component in this context but with an extension point. One can provide custom content for this extension point or use the default. There might be multiple extension points, which would be implemented as named scoped slots in VueJS for example.
function MyButtonComponent({ buttonText } = {}) {
if (!buttonText) {
buttonText = (level) => `My Powerful Button (level: ${level})`
}
const computeLevelFromContent = (content) => content.length
const level = computeLevelFromContent(buttonText(""))
const content = buttonText(level)
return `<button>${content}</button>`
}
function Parent() {
return `${MyButtonComponent()} ${MyButtonComponent({
buttonText: (level) => `${level} My Extended Button π`,
})}`
}
Parent()
function MyButtonComponent({ buttonText } = {}) {
if (!buttonText) {
buttonText = (level) => `My Powerful Button (level: ${level})`
}
const computeLevelFromContent = (content) => content.length
const level = computeLevelFromContent(buttonText(""))
const content = buttonText(level)
return `<button>${content}</button>`
}
function Parent() {
return `${MyButtonComponent()} ${MyButtonComponent({
buttonText: (level) => `${level} My Extended Button π`,
})}`
}
Parent()
Result
Nesting
When we nest components while using previous techniques we end up passing content from parent components down into nested child components. Slots inside slots. Maximum adaptability but also maximum cognitive overhead.
In Vue placing a slot that is passed into another componentβs slot is not that bad, but Iβm not going to write a convoluted vanilla javascript example. This piece of Vue template shows a slot that a parent component can use to pass content down into AdaptableComponent and use all its props.
One interesting aspect of this is that we can decide where we want the content to come from and where in the hierarchy the default content will be placed.
<template>
<AdaptableComponent>
<template #default="scope">
<slot v-bind="scope" />
</template>
</AdaptableComponent>
</template>