Scalable Prop Patterns with Vue.js
Please fill out this quick survey!
Get resources for workshop:
My Background
What's my experience?
Ben Hong
Vue Core Team
Vue Mastery Instructor
DX Engineer at Cypress.io
What about you?
What about you?
- Name
- Title
- Fun Fact / Hobby
- Top Goal
Zoom Onboarding
Format
Format
1. Learn
2. Question
3. Apply
Format
1. Learn
2. Question
3. Apply
concepts
examples
stories
clarification
what-ifs
code
experiment
one-on-one help
Resources
Scalable Prop Patterns GitHub Repo
Your own projects!
Participation Tips
"Raise your hand"
for questions at any time!
All examples are public.
(no need to copy down code examples)
Please do not record
(out of respect for the privacy of participants)
Questions?
Props
Props
Props are custom attributes you can register on a component.
Props allow us to pass data
into a component.
Defining Props
Defining Props
Array Syntax
<script>
export default {
props: [
'title',
'author',
'genre'
]
}
</script>
Great for prototyping,
but not very helpful otherwise...
Defining Props
Prop Types
Allows you to define basic validation for your props
Common Prop Types
- String
- Number
- Boolean
- Array
- Object
Lesser Known
- Date
- Function
- Symbol
Defining Props
Array Syntax
<script>
export default {
props: [
'title',
'author',
'genre'
]
}
</script>
Defining Props
Adding a Prop Type
<script>
export default {
props: {
title: String,
author: String,
genre: String
}
}
</script>
Defining Props
Adding Prop Types
<script>
export default {
props: {
title: String,
author: [String, Object],
genre: [String, Array]
}
}
</script>
Defining Props
Is the prop important?
Defining Props
Is the prop important?
<script>
export default {
props: {
title: {
type: String,
},
author: {
type: [String, Object],
},
genre: {
type: [String, Array],
}
}
}
</script>
<script>
export default {
props: {
title: {
type: String,
required: true
},
author: {
type: [String, Object],
required: true
},
genre: {
type: [String, Array],
required: false
}
}
}
</script>
Defining Props
Is the prop important?
Defining Props
How will the prop be used most of the time?
Defining Props
<script>
export default {
props: {
title: {
type: String,
required: true
},
author: {
type: [String, Object],
required: true
},
genre: {
type: [String, Array],
required: false
}
}
}
</script>
How will the prop be used most of the time?
Defining Props
<script>
export default {
props: {
title: {
type: String,
required: true
},
author: {
type: [String, Object],
required: true
},
genre: {
type: [String, Array],
required: false,
default: 'Uncategorized'
}
}
}
</script>
How will the prop be used most of the time?
Defining Props
<script>
export default {
props: {
title: {
type: String,
required: true
},
author: {
type: [String, Object],
required: true
},
genre: {
type: [String, Array],
required: false,
default: 'Uncategorized'
}
}
}
</script>
How will the prop be used most of the time?
Defining Props
<script>
export default {
props: {
title: {
type: String,
required: true
},
author: {
type: [String, Object],
required: true,
default: { name: 'Unknown' }
},
genre: {
type: [String, Array],
required: false,
default: 'Uncategorized'
}
}
}
</script>
How will the prop be used most of the time?
Defining Props
There is a caveat to prop default values...
Defining Props
There is a caveat to prop default values...
Defining Props
<script>
// Dice.vue
export default {
props: {
colors: {
type: Array,
},
currentValue: {
type: Number,
default: 1
},
material: {
type: Object
}
}
}
</script>
Defining Props
<script>
// Dice.vue
export default {
props: {
colors: {
type: Array,
default: () => ([])
},
currentValue: {
type: Number,
default: 1
},
material: {
type: Object,
default: () => ({})
}
}
}
</script>
Use "factory" functions to generate arrays and objects
Defining Props
<script>
// Dice.vue
export default {
props: {
colors: {
type: Array,
default: () => ([])
},
currentValue: {
type: Number,
default: () => {
return Math.floor(Math.random() * 6 + 1)
}
},
material: {
type: Object,
default: () => ({})
}
}
}
</script>
You can generate dynamic default values!
Going back to our
Book example...
Defining Props
<script>
export default {
props: {
title: {
type: String,
required: true,
},
author: {
type: [String, Object],
required: true,
default: { name: 'Unknown' }
},
genre: {
type: [String, Array],
required: false,
default: 'Uncategorized'
}
}
}
</script>
How will the prop be used most of the time?
Defining Props
<script>
export default {
props: {
title: {
type: String,
required: true,
},
author: {
type: [String, Object],
required: true,
default: () => ({ name: 'Unknown' })
},
genre: {
type: [String, Array],
required: false,
default: 'Uncategorized'
}
}
}
</script>
How will the prop be used most of the time?
Defining Props
Looks good, but...
<script>
export default {
props: {
title: {
type: String,
required: true,
},
author: {
type: [String, Object],
required: true,
default: () => ({})
},
genre: {
type: [String, Array],
required: false,
default: 'Uncategorized'
}
}
}
</script>
Defining Props
You only need one
<script>
export default {
props: {
title: {
type: String,
required: true,
},
author: {
type: [String, Object],
required: true,
default: () => ({})
},
genre: {
type: [String, Array],
required: false,
default: 'Uncategorized'
}
}
}
</script>
Defining Props
You only need one
<script>
export default {
props: {
title: {
type: String,
required: true,
},
author: {
type: [String, Object],
default: () => ({})
},
genre: {
type: [String, Array],
required: false,
default: 'Uncategorized'
}
}
}
</script>
Defining Props
💡Tip: You just need one
<script>
export default {
props: {
title: {
type: String,
required: true,
},
author: {
type: [String, Object],
default: () => ({})
},
genre: {
type: [String, Array],
default: 'Uncategorized'
}
}
}
</script>
Object Syntax
Array Syntax
<script>
export default {
props: [
'title',
'author',
'genre'
]
}
</script>
Defining Props
<script>
export default {
props: {
title: {
type: String,
required: true,
},
author: {
type: [String, Object],
default: () => ({})
},
genre: {
type: [String, Array],
default: 'Uncategorized'
}
}
}
</script>
Props
Props are custom attributes you can register on a component.
Props allow us to pass data
into a component.
Props
Props are custom attributes you can register on a component.
Props allow us to pass data
into a component.
Props provides guidance for future developers
Any questions?
Let's code!
In the repo
- Refactor the BaseFooter.vue component to leverage object syntax
In your app
- Look at how the props are defined and see if there are any improvements you can make
Practice
This is my info alert box
This is my info alert box
<script>
// AlertBox.vue
export default {
props: {
type: {
type: String,
default: 'info'
}
}
}
</script>
<template>
<aside class="alert-box" :class="`is-${type}`">
{{ text }}
</aside>
</template>
This is my info alert box
This is my success alert box
This is my warning alert box
This is my danger alert box
This is my info alert box
This is my success alert box
This is my warning alert box
This is my danger alert box
<template>
<AlertBox type="info" />
<AlertBox type="success" />
<AlertBox type="warning" />
<AlertBox type="danger" />
</template>
The standard prop validations is pretty great...
But have you ever tried custom validators?
The standard prop validations is pretty great...
This is my info alert box
<script>
// AlertBox.vue
export default {
props: {
type: {
type: String,
default: 'info'
}
}
}
</script>
<template>
<aside class="alert-box" :class="`is-${type}`">
{{ text }}
</aside>
</template>
This is my info alert box
<script>
// AlertBox.vue
export default {
props: {
type: {
type: String,
default: 'info'
}
}
}
</script>
This is my info alert box
<script>
// AlertBox.vue
export default {
props: {
type: {
type: String,
default: 'info',
validator: (propValue) => {
return propValue.length > 0
}
}
}
}
</script>
<script>
export default {
validator: (propValue) => {
return propValue.length > 0
}
}
</script>
<script>
export default {
validator: (propValue) => {
return propValue.length > 0
}
}
</script>
<script>
export default {
validator: (propValue) => {
return propValue.length > 0
}
}
</script>
This is my info alert box
<script>
// AlertBox.vue
export default {
props: {
type: {
type: String,
default: 'info',
validator: (propValue) => {
return propValue.length > 0
}
}
}
}
</script>
This is my info alert box
<script>
// AlertBox.vue
export default {
props: {
type: {
type: String,
default: 'info',
validator: (propValue) => {
const validTypes = ['info', 'success', 'warning', 'danger']
return validTypes.indexOf(propValue) > -1
}
}
}
}
</script>
Any questions?
But what if you wanted more complex validation...
<script>
// BaseLink.vue
export default {
props: {
href: {
type: String,
default: '',
},
allowInsecure: {
type: Boolean,
default: false,
}
}
</script>
<script>
// BaseLink.vue
export default {
props: {
href: {
type: String,
default: '',
},
allowInsecure: {
type: Boolean,
default: false,
}
},
created() {
this.validateProps()
}
}
</script>
<script>
// BaseLink.vue
export default {
props: {
href: {
type: String,
default: '',
},
allowInsecure: {
type: Boolean,
default: false,
}
},
created() {
this.validateProps()
},
methods: {
validateProps() {
}
}
}
</script>
<script>
// BaseLink.vue
export default {
props: {
href: {
type: String,
default: '',
},
allowInsecure: {
type: Boolean,
default: false,
}
},
created() {
this.validateProps()
},
methods: {
validateProps() {
if (this.href) {
if (!this.allowInsecure && !/^(https|mailto|tel):/.test(this.href)) {
return console.warn(
`Insecure <BaseLink> href: ${this.href}.\n.
If this site does not offer SSL, explicitly add the
allow-insecure attribute on <BaseLink>.`
)
}
}
}
}
}
</script>
<script>
// BaseLink.vue
export default {
props: {
href: {
type: String,
default: '',
},
allowInsecure: {
type: Boolean,
default: false,
}
},
created() {
this.validateProps()
},
methods: {
validateProps() {
if (process.env.NODE_ENV === 'production') return
if (this.href) {
if (!this.allowInsecure && !/^(https|mailto|tel):/.test(this.href)) {
return console.warn(
`Insecure <BaseLink> href: ${this.href}.\n.
If this site does not offer SSL, explicitly add the
allow-insecure attribute on <BaseLink>.`
)
}
}
}
}
}
</script>
Any questions?
Let's code!
In the repo
- Refactor the BaseBadge.vue component to leverage custom validation
In your app
- See if there are any components in your app that can leverage custom prop validators
Practice
Prop Patterns
Prop Train Pattern
Prop Train Pattern
<!-- MyLibraryPage.vue -->
<template>
<Library :preferences="preferences">
<Bookshelf v-for="bookshelf in bookshelves" :key="bookshelf.id" :preferences="preferences">
<Book v-for="book in bookshelf.books" :key="book.id" :preferences="preferences">
<Page v-for="page in book.pages" :key="page.id" :preferences="preferences" />
</Book>
</Bookshelf>
</Library>
</template>
<!-- MyLibraryPage.vue -->
<template>
<Library :preferences="preferences">
<Bookshelf v-for="bookshelf in bookshelves" :preferences="preferences">
<Book v-for="book in bookshelf.books" :preferences="preferences">
<Page v-for="page in book.pages" :preferences="preferences" />
</Book>
</Bookshelf>
</Library>
</template>
Prop Train Pattern
Prop Train Pattern
<!-- MyLibraryPage.vue -->
<template>
<Library :preferences="preferences">
<Bookshelf v-for="bookshelf in bookshelves" :preferences="preferences">
<Book v-for="book in bookshelf.books" :preferences="preferences">
<Page v-for="page in book.pages" :preferences="preferences" />
</Book>
</Bookshelf>
</Library>
</template>
Prop Train Pattern
This is typically a sign to refactor your props.
- Vuex
- Provide / Inject
Any questions?
Prop Composition
Prop Composition
Leverages the computed property to allow you to take props and either break them down or add complexity
Prop Composition
<script>
export default {
name: 'BaseDateLabel',
props: {
isoDate: {
/** ISO-8601 format **/
type: String,
required: true
}
}
}
</script>
<template>
<p>
{{ new Date(isoDate) }}
</p>
</template>
Prop Composition
<script>
export default {
name: 'BaseDateLabel',
props: {
isoDate: {
/** ISO-8601 format **/
type: String,
required: true
}
},
computed: {
date() {
return new Date(this.isoDate)
}
}
}
</script>
<template>
<p>
{{ date }}
</p>
</template>
Prop Composition
<script>
export default {
name: 'BaseDateLabel',
props: {
isoDate: {
/** ISO-8601 format **/
type: String,
required: true
}
},
computed: {
date() {
return new Date(this.isoDate)
}
}
}
</script>
<template>
<p>
{{ date.getFullYear() }}-{{ date.getMonth() + 1 }}-{{ date.getDate() }}
</p>
</template>
Prop Composition
<script>
export default {
name: 'BaseDateLabel',
props: {
isoDate: {
/** ISO-8601 format **/
type: String,
required: true
}
},
computed: {
date() {
return new Date(this.isoDate)
},
simpleDate() {
return `${this.date.getFullYear()}-${this.date.getMonth() + 1}-${this.date.getDate()}`
}
}
}
</script>
<template>
<p>
{{ simpleDate }}
</p>
</template>
<script>
export default {
name: 'BaseDateLabel',
props: {
isoDate: {
/** ISO-8601 format **/
type: String,
required: true
}
},
computed: {
date() {
return new Date(this.isoDate)
},
simpleDate() {
return `${this.date.getFullYear()}-${this.date.getMonth() + 1}-${this.date.getDate()}`
},
dayNumber() {
return this.date.getDate()
},
monthNumber() {
return this.date.getMonth() + 1
},
yearNumber() {
return this.date.getFullYear()
},
}
}
</script>
<template>
<p>
{{ simpleDate }}
</p>
</template>
<script>
export default {
name: 'BaseDateLabel',
props: {
isoDate: {
/** ISO-8601 format **/
type: String,
required: true
}
},
computed: {
date() {
return new Date(this.isoDate)
},
simpleDate() {
return `${this.yearNumber}-${this.monthNumber}-${this.dayNumber}`
},
dayNumber() {
return this.date.getDate()
},
monthNumber() {
return this.date.getMonth() + 1
},
yearNumber() {
return this.date.getFullYear()
},
}
}
</script>
<template>
<p>
{{ simpleDate }}
</p>
</template>
Any questions?
Let's code!
In the repo
- Enhance BaseDateLabel.vue
to allow users to toggle what format is displayed to the user
In your app
- Analyze your existing components for opportunities to use prop composition
- Identify opportunities for refactoring components using the prop train pattern
Practice
"Best" Prop Practices
Do not mutate
your props
Do not mutate
your props
<script>
// Counter.vue
export default {
props: {
startingValue: Number
},
methods: {
incrementValue() {
this.startingValue++
}
}
}
</script>
<template>
<div class="counter">
<p>{{ startingValue }}
<button @click="incrementValue">Increase by 1</button>
</div>
</template>
Do not mutate
your props
<script>
// Counter.vue
export default {
props: {
startingValue: Number
},
methods: {
incrementValue() {
this.startingValue++
}
}
}
</script>
<template>
<div class="counter">
<p>{{ startingValue }}</p>
<button @click="incrementValue">Increase by 1</button>
</div>
</template>
<script>
// Counter.vue
export default {
props: {
startingValue: Number
},
data: () => ({
currentValue: 0
}),
methods: {
incrementValue() {
this.currentValue++
}
},
created() {
this.currentValue = this.startingValue
}
}
</script>
<template>
<div class="counter">
<p>{{ startingValue }}</p>
<button @click="incrementValue">Increase by 1</button>
</div>
</template>
Do not mutate
your props
Any questions?
Alphabetize
your props
Alphabetize
your props
<script>
// Book.vue
export default {
props: {
title: String,
author: [Array, String],
publishDate: [Date, String],
publisher: [Object, String],
length: Number,
editions: [Array, Number],
genre: [Array, String],
isbn: String,
vendors: Array,
editor: [Object, String],
website: String
}
}
</script>
Alphabetize
your props
<script>
// Book.vue
export default {
props: {
author: [Array, String],
editions: [Array, Number],
editor: [Object, String],
genre: [Array, String],
isbn: String,
length: Number,
publishDate: [Date, String],
publisher: [Object, String],
title: String,
vendors: Array,
website: String,
},
}
</script>
Any questions?
Comments
are valuable
Comments
are valuable
<script>
// Book.vue
export default {
props: {
author: [Array, String],
editions: [Array, Number],
editor: [Object, String],
genre: [Array, String],
isbn: String,
length: Number,
publishDate: [Date, String],
publisher: [Object, String],
title: String,
vendors: Array,
website: String,
},
}
</script>
Comments
are valuable
<script>
// Book.vue
export default {
props: {
author: [Array, String],
editions: [Array, Number],
editor: [Object, String],
genre: [Array, String],
isbn: String,
/** Number of pages in the printed edition **/
length: Number,
publishDate: [Date, String],
publisher: [Object, String],
title: String,
vendors: Array,
website: String,
},
}
</script>
Any questions?
Avoid generic
prop names
Avoid generic
prop names
<script>
// Book.vue
export default {
props: {
author: [Array, String],
editions: [Array, Number],
editor: [Object, String],
genre: [Array, String],
isbn: String,
length: Number,
publishDate: [Date, String],
publisher: [Object, String],
title: String,
vendors: Array,
website: String,
}
}
</script>
Avoid generic
prop names
<script>
// Book.vue
export default {
props: {
data: {
type: Object,
required: true
}
}
}
</script>
Avoid generic
prop names
<script>
// Book.vue
export default {
props: {
data: {
type: Object,
required: true
}
},
data: () => ({
// Local data store
})
}
</script>
Avoid generic
prop names
<script>
// Book.vue
export default {
props: {
data: {
type: Object,
required: true
}
},
data: () => ({
// Local data store
})
}
</script>
In addition to data, some other prop names to be cautious of include:
- item
- option
- response
- payload
Any questions?
Avoid using a
CSS class prop
<script>
export default {
name: 'BaseButton',
props: {
className: {
type: Array
},
text: {
type: String,
default: 'Submit'
}
},
};
</script>
<template>
<button :class="['button', ...className]">
{{ text }}
</button>
</template>
<template>
<BaseButton :className="['is-primary', 'is-outlined']" />
</template>
Avoid using a
CSS class prop
<script>
export default {
name: 'BaseButton',
props: {
className: {
type: Array
},
text: {
type: String,
default: 'Submit'
}
},
};
</script>
<template>
<button :class="['button', ...className]">
{{ text }}
</button>
</template>
Avoid using a
CSS class prop
Avoid using props for CSS classes
<script>
export default {
name: 'BaseButton',
props: {
text: {
type: String,
default: 'Submit'
}
},
};
</script>
<template>
<button class="button">
{{ text }}
</button>
</template>
<template>
<BaseButton class="is-primary is-outlined" />
</template>
<button class="button is-primary is-outlined">
Submit
</button>
Avoid using props for CSS classes
However, there is one exception...
However, there is
one exception...
Component library
<script>
export default {
name: "ComponentLibraryButton",
props: {
className: {
type: String,
default: "button is-primary"
},
text: {
type: String,
default: "Submit"
}
}
}
</script>
<template>
<button :class="className">
{{ text }}
</button>
</template>
Any questions?
Open Practice
Q&A
Help me improve my workshop and decide what topic
to teach next!
https://bencodezen.typeform.com/to/qktOXj
Scalable Prop Patterns - June 2020
By Ben Hong
Scalable Prop Patterns - June 2020
In this workshop, you will be guided through fundamental prop techniques, best practices and patterns for creating scalable Vue.js components.
- 1,043