vue-patterns
Useful Vue patterns, techniques, tips and tricks and helpful curated links.
- Component Declaration
- Component Communication
- Component Conditional Rendering
- Dynamic Component
- Composition
- Passing Props
- Higher Order Component (a.k.a. HOC)
- Dependency injection
- Handling Errors
- Productivity Tips
- Useful Links
Component Declaration
Single File Component (a.k.a. SFC) - Most Common
<template>
<button class="btn-primary" @click.prevent="handleClick">
{{text}}
</button>
</template>
<script>
export default {
data() {
return {
text: 'Click me',
};
},
methods: {
handleClick() {
console.log('clicked');
},
},
}
</script>
<style scoped>
.btn-primary {
background-color: blue;
}
</style>
String Template (or ES6 Template Literal)
Vue.component('my-btn', {
template: `
<button class="btn-primary" @click.prevent="handleClick">
{{text}}
</button>
`,
data() {
return {
text: 'Click me',
};
},
methods: {
handleClick() {
console.log('clicked');
},
},
});
Render Function
Vue.component('my-btn', {
data() {
return {
text: 'Click me',
};
},
methods: {
handleClick() {
console.log('clicked');
},
},
render(h) {
return h('button', {
attrs: {
class: 'btn-primary'
},
on: {
click: this.handleClick,
},
});
},
});
JSX
Vue.component('my-btn', {
data() {
return {
text: 'Click me',
};
},
methods: {
handleClick() {
console.log('clicked');
},
},
render() {
return (
<button class="btn-primary" onClick={this.handleClick}>
{{this.text}}
</button>
);
},
});
vue-class-component
<template>
<button class="btn-primary" @click.prevent="handleClick">
{{text}}
</button>
</template>
<script>
import Vue from 'vue';
import Component from 'vue-class-component';
@Component
export default MyBtn extends Vue {
text = 'Click me';
handleClick() {
console.log('clicked');
}
}
</script>
<style scoped>
.btn-primary {
background-color: blue;
}
</style>
References:
Component Communication
Props and Events
Basically, vue component follows one-way data flow, that is props down(See official guide) and event up.
Props are read-only data, so it's impossible to change props from child components.
When props changes, child components will be rerendered automatically(props are reactive data source).
Child components can only emit event to direct parent, so that the parent component may change data
, mapped to the child component's props
.
<template>
<button @click="$emit('click')">{{text}}</button>
</template>
<script>
export default {
name: 'v-btn',
props: {
text: String,
},
};
</script>
<template>
<v-btn :text="buttonText" @click="handleClick"></v-btn>
</template>
<script>
export default {
data() {
return {
clickCount: 0,
buttonText: 'initial button text',
};
},
methods: {
handleClick() {
this.buttonText = `Button clicked ${++this.clickCount}`;
console.log('clicked', this.buttonText);
}
}
};
</script>
References:
- Vue.js Component Communication Patterns
- Creating Custom Inputs With Vue.js
- Vue Sibling Component Communication
- Managing State in Vue.js
Component Events Handling
References:
- Leveraging Vue events to reduce prop declarations
- Vue.js Component Hooks as Events
- Creating a Global Event Bus with Vue.js
- Vue.js Event Bus + Promises
Component Conditional Rendering
Directives (v-if
/ v-else
/ v-else-if
/ v-show
)
v-if
<h1 v-if="true">Render only if v-if condition is true</h1>
v-if
and v-else
<h1 v-if="true">Render only if v-if condition is true</h1>
<h1 v-else>Render only if v-if condition is false</h1>
v-else-if
<div v-if="type === 'A'">Render only if `type` is equal to `A`</div>
<div v-else-if="type === 'B'">Render only if `type` is equal to `B`</div>
<div v-else-if="type === 'C'">Render only if `type` is equal to `C`</div>
<div v-else>Render if `type` is not `A` or `B` or `C`</div>
v-show
<h1 v-show="true">Always rendered, but it should be visible only if `v-show` conditions is true</h1>
If you want to conditionally render more than one element,
you can use directives(v-if
/ v-else
/ v-else-if
/v-show
) on a <template>
element.
Notice that <template>
element is not actually rendered into DOM. It is an invisible wrapper.
<template v-if="true">
<h1>All the elements</h1>
<p>will be rendered into DOM</p>
<p>except `template` element</p>
</template>
JSX
If you use JSX in your vue application, you can apply all the techniques such as if else
and switch case
statement and ternary
and logical
operator.
if else
statement
export default {
data() {
return {
isTruthy: true,
};
},
render(h) {
if (this.isTruthy) {
return <h1>Render value is true</h1>;
} else {
return <h1>Render value is false</h1>;
}
},
};
switch case
statement
import Info from './Info';
import Warning from './Warning';
import Error from './Error';
import Success from './Success';
export default {
data() {
return {
type: 'error',
};
},
render(h) {
switch (this.type) {
case 'info':
return <Info text={text} />;
case 'warning':
return <Warning text={text} />;
case 'error':
return <Error text={text} />;
default:
return <Success text={text} />;
}
},
};
or you can use object
map to simplify switch case
import Info from './Info';
import Warning from './Warning';
import Error from './Error';
import Success from './Success';
const COMPONENT_MAP = {
info: Info,
warning: Warning,
error: Error,
success: Success,
};
export default {
data() {
return {
type: 'error',
};
},
render(h) {
const Comp = COMPONENT_MAP[this.type || 'success'];
return <Comp />;
},
};
ternary
operator
export default {
data() {
return {
isTruthy: true,
};
},
render(h) {
return (
<div>
{this.isTruthy ? (
<h1>Render value is true</h1>
) : (
<h1>Render value is false</h1>
)}
</div>
);
},
};
logical
operator
export default {
data() {
return {
isLoading: true,
};
},
render(h) {
return <div>{this.isLoading && <h1>Loading ...</h1>}</div>;
},
};
References
Dynamic Component
<component>
with is
attribute
<component :is="currentTabComponent"></component>
With the above code example, rendered component will be destroyed if a different component is rendered in <component>
. If you want components to keep their instances without being destroyed within <component>
tag, you can wrap the <component>
tag in a <keep-alive>
tag like so:
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
References
Composition
Library
Basic Composition
<template>
<div class="component-b">
<component-a></component-a>
</div>
</template>
<script>
import ComponentA from './ComponentA';
export default {
components: {
ComponentA,
},
};
</script>
Extends
When you want to extend a single vue component
<template>
<button class="button-primary" @click.prevent="handleClick">
{{buttonText}}
</button>
</template>
<script>
import BaseButton from './BaseButton';
export default {
extends: BaseButton,
props: ['buttonText'],
};
</script>
References:
Mixins
// closableMixin.js
export default {
props: {
isOpen: {
default: true
}
},
data: function() {
return {
shown: this.isOpen
}
},
methods: {
hide: function() {
this.shown = false;
},
show: function() {
this.shown = true;
},
toggle: function() {
this.shown = !this.shown;
}
}
}
<template>
<div v-if="shown" class="alert alert-success" :class="'alert-' + type" role="alert">
{{text}}
<i class="pull-right glyphicon glyphicon-remove" @click="hide"></i>
</div>
</template>
<script>
import closableMixin from './mixins/closableMixin';
export deafult {
mixins: [closableMixin],
props: ['text']
};
</script>
References:
Slots (Default)
<template>
<button class="btn btn-primary">
<slot></slot>
</button>
</template>
<script>
export default {
name: 'VBtn',
};
</script>
<template>
<v-btn>
<span class="fa fa-user"></span>
Login
</v-btn>
</template>
<script>
import VBtn from './VBtn';
export default {
components: {
VBtn,
}
};
</script>
References:
- Understanding Component Slots with Vue.js
- Composing Custom Elements With Slots And Named Slots
- Writing Abstract Components with Vue.js
Named Slots
BaseLayout.vue
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
App.vue
<base-layout>
<template slot="header">
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template slot="footer">
<p>Here's some contact info</p>
</template>
</base-layout>
Scoped Slots
<template>
<ul>
<li
v-for="todo in todos"
v-bind:key="todo.id"
>
<!-- We have a slot for each todo, passing it the -->
<!-- `todo` object as a slot prop. -->
<slot v-bind:todo="todo">
{{ todo.text }}
</slot>
</li>
</ul>
</template>
<script>
export default {
name: 'TodoList',
props: {
todos: {
type: Array,
default: () => ([]),
}
},
};
</script>
<template>
<todo-list v-bind:todos="todos">
<template slot-scope="{ todo }">
<span v-if="todo.isComplete">✓</span>
{{ todo.text }}
</template>
</todo-list>
</template>
<script>
import TodoList from './TodoList';
export default {
components: {
TodoList,
},
data() {
return {
todos: [
{ todo: 'todo 1', isComplete: true },
{ todo: 'todo 2', isComplete: false },
{ todo: 'todo 3', isComplete: false },
{ todo: 'todo 4', isComplete: true },
];
};
},
};
</script>
References:
- Getting Your Head Around Vue.js Scoped Slots
- Understanding scoped slots in Vue.js
- Scoped Component Slots in Vue.js
- The Trick to Understanding Scoped Slots in Vue.js
- The Power of Scoped Slots in Vue
Render Props
In most cases, you can use scoped slots instead of render props. But, it might be useful in some case.
with SFC
<template>
<div id="app">
<Mouse :render="__render"/>
</div>
</template>
<script>
import Mouse from "./Mouse.js";
export default {
name: "app",
components: {
Mouse
},
methods: {
__render({ x, y }) {
return (
<h1>
The mouse position is ({x}, {y})
</h1>
);
}
}
};
</script>
<style>
* {
margin: 0;
height: 100%;
width: 100%;
}
</style>
with JSX
const Mouse = {
name: "Mouse",
props: {
render: {
type: Function,
required: true
}
},
data() {
return {
x: 0,
y: 0
};
},
methods: {
handleMouseMove(event) {
this.x = event.clientX;
this.y = event.clientY;
}
},
render(h) {
return (
<div style={{ height: "100%" }} onMousemove={this.handleMouseMove}>
{this.$props.render(this)}
</div>
);
}
};
export default Mouse;
References:
Passing Props
Sometimes, you may want to pass props and listeners to child component without having to declare all child component's props.
You can simply bind $attrs
and $listeners
to child component
<template>
<div>
<h1>{{title}}</h1>
<child-component v-bind="$attrs" v-on="$listeners"></child-component>
</div>
</template>
<script>
export default {
name: 'PassingPropsSample'
props: {
title: {
type: String,
default: 'Hello, Vue!'
}
}
};
</script>
From parent component, you can do like this:
<template>
<passing-props-sample
title="Hello, Passing Props"
childPropA="This props will properly mapped to <child-component />"
@click="handleChildComponentClick"
>
</passing-props-sample>
</template>
<script>
import PassingPropsSample from './PassingPropsSample';
export default {
components: {
PassingPropsSample
},
methods: {
handleChildComponentClick() {
console.log('child component clicked');
}
}
};
</script>
References:
Higher Order Component (a.k.a. HOC)
References:
- Higher Order Components in Vue.js
- Do we need Higher Order Components in Vue.js?
- Higher-Order Components in Vue.js
Dependency injection
Vue supports provide / inject mechanism to provide object
into all its descendants, regardless of how deep the component hierarchy is, as long as they are in the same parent chain. Notice that provide
and inject
bindings are not reactive, unless you pass down an observed object.
<parent-component>
<child-component>
<grand-child-component></grand-child-component>
</child-component>
</ancestor-component>
With above example component hierarchy, in order to derive data from parent-component
, you should pass down data(object) as props
to child-component
and grand-child-component
. However, if parent-component
provide
data(object), grand-child-component
can just define inject
provided object from parent-component
.
References:
- Official API
- Official Guide
- Component Communication
- Dependency Injection in Vue.js App with TypeScript
Provide / Inject
// ParentComponent.vue
export default {
provide: {
theme: {
primaryColor: 'blue',
},
},
};
// GrandChildComponent.vue
<template>
<button :style="{ backgroundColor: primary && theme.primaryColor }">
<slot></slot>
</button>
</template>
<script>
export default {
inject: ['theme'],
props: {
primary: {
type: Boolean,
default: true,
},
},
};
</script>
@Provide / @Inject Decorator
// ParentComponent.vue
import { Component, Vue, Provide } from 'vue-property-decorator';
@Component
export class ParentComponent extends Vue {
@Provide
theme = {
primaryColor: 'blue',
};
}
// GrandChildComponent.vue
<template>
<button :style="{ backgroundColor: primary && theme.primaryColor }">
<slot></slot>
</button>
</template>
<script>
import { Component, Vue, Inject, Prop } from 'vue-property-decorator';
export class GrandChildComponent extends Vue {
@Inject() theme;
@Prop({ default: true })
primary: boolean;
};
</script>
Handling Errors
errorCaptured
Hook
export default {
name: 'ErrorBoundary',
data() {
return {
error: false,
errorMessage: '',
};
},
errorCaptured (err, vm, info) {
this.error = true;
this.errorMessage = `${err.stack}\n\nfound in ${info} of component`;
return false;
},
render (h) {
if (this.error) {
return h('pre', { style: { color: 'red' }}, this.errorMessage);
}
return this.$slots.default[0]
}
};
<error-boundary>
<another-component/>
</error-boundary>
Examples
References
Productivity Tips
watch on create
// don't
created() {
this.fetchUserList();
},
watch: {
searchText: 'fetchUserList',
}
// do
watch: {
searchText: {
handler: 'fetchUserList',
immediate: true,
}
}
Useful Links
Refactoring
- Refactoring Vue: Cleaning Up a List of Posts With Better Component Splitting and More ES6
- Clean up your Vue modules with ES6 Arrow Functions
- Examples of Vue’s Clean Code
- Optimizing Performance with Computed Properties
State Management
Vuex
- Decouple Vuex modules with the Mediator pattern
- Vuex getters are great, but don’t overuse them
- Reusable Vuex Mutation Functions
- A pattern to handle ajax requests in Vuex
- [vuex Mutations] Single Changes vs. Single Responsibility
- Components and How They Interact in Vue and Vuex
- Why VueX Is The Perfect Interface Between Frontend and API
- Composing actions with Vuex
- How to Build Complex, Large-Scale Vue.js Apps With Vuex
- Should I Store This Data in Vuex?
Mobx
Renderless Component
Examples
Folder Structure
- How you can improve your workflow using the JavaScript console
- How to Structure a Vue.js Project
- Large-scale Vuex application structures
- Vue.js Application Structure and CSS Architecture
Tips & Tricks
- How To Build Vue Components Like A Pro 😎
- Four tips for working with Vue.js
- Tips from a Lowly VueJS Developer
- Throttling and Debouncing Events with Vue.js and lodash
- Are partially applied functions in event handlers possible?
- Vue.js — Considerations and Tricks
- Six random issues and their solutions in VueJS.
- When VueJS Can't Help You
- Things that won’t work using Vue
- Tip#15 Delay execution with _.debounce
Typescript
Flowtype
Router
GraphQL
Repos
Anti Patterns
- Chris Fritz - Vue.js Anti-Patterns (and How to Avoid Them)
- Common mistakes to avoid while working with Vue.js
- Avoid This Common Anti-Pattern In Full-Stack Vue/Laravel Apps
Videos / Audios
- 81: Evan You - Advanced Vue Component Design
- 7 Secret Patterns Vue Consultants Don’t Want You to Know
Paid
Misc
- Creating an Interpose Vue component from a React implementation
- Composing computed properties in Vue.js
- 4 AJAX Patterns For Vue.js Apps
- 3 Code Splitting Patterns For VueJS and Webpack
- The easiest way to improve your Vue.js application. Part 1
- Using JSX with Vue and Why You Should Care
- Compound components
- Creating Multi-root Vue.js Components
- Understanding Vue.js Reactivity in Depth with Object.defineProperty()
- Templating in Vue: Separation of Concerns or Separation of Technology or something else?
- Stashing Vue components data
- Creating Reusable Transitions in Vue
- vue-advanced-workshop
- Do it with Elegance: How to Create Data-Driven User Interfaces in Vue
- Creating Vue.js Component Instances Programmatically
- Managing User Permissions in a Vue.js App
- Render Functional Components in Vue.js
- Templating in Vue: Separation of Concerns or Separation of Technology or something else?
- Looping through Object Properties
- Cancelling async operations in Vue.js
- Scoped styles with v-html
- Pagination With Vuejs
- What does the ‘h’ stand for in Vue’s render method?
- How To Build Vue Components That Play Nice
- Making responsive Vue components with ResizeObserver
- An imperative guide to forms in Vue.js
- Vue.js: the good, the meh, and the ugly
- Dynamic Vue.js Layout Components
- Advanced Vue.js concepts: mixins, custom directives, filters, transitions, and state management
- Introducing the Single Element Pattern
- Control DOM Outside Your Vue.js App with portal-vue
- Add i18n and manage translations of a Vue.js powered website