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.

  1. Vuex
  2. 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.

  • 940