Useful Vue patterns, techniques, tips and tricks and helpful curated links.
- Component Declaration
- Component Communication
- Component Events Handling
- Component Conditional Rendering
- Dynamic Component
- Composition
- Passing Props
- Higher Order Component (a.k.a. HOC)
- Dependency injection
- Handling Errors
- Productivity Tips
- Useful Links
<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>
Vue.component('my-btn', {
template: `
<button class="btn-primary" @click.prevent="handleClick">
{{text}}
</button>
`,
data() {
return {
text: 'Click me',
};
},
methods: {
handleClick() {
console.log('clicked');
},
},
});
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,
},
});
},
});
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>
);
},
});
<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>
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>
- Official - Props
- Vue.js Component Communication Patterns
- Creating Custom Inputs With Vue.js
- Vue Sibling Component Communication
- Managing State in Vue.js
- Official - Custom Events
- 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
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>
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>;
},
};
<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>
- Official - Dynamic Components
- Official - Dynamic & Async Components
- Dynamic Component Templates with Vue.js
<template>
<div class="component-b">
<component-a></component-a>
</div>
</template>
<script>
import ComponentA from './ComponentA';
export default {
components: {
ComponentA,
},
};
</script>
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>
// 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 default {
mixins: [closableMixin],
props: ['text']
};
</script>
<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>
- Official - Slot Content
- Understanding Component Slots with Vue.js
- Composing Custom Elements With Slots And Named Slots
- Writing Abstract Components with Vue.js
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>
<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>
- Official - Scoped Slots
- 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
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;
Sometimes, you may want to pass props and listeners to child component without having to declare all child component's props.
You can bind $attrs
and $listeners
in child component and set inheritAttrs
to false
(otherwise both, div
and child-component
will receive the attributes)
<template>
<div>
<h1>{{title}}</h1>
<child-component v-bind="$attrs" v-on="$listeners"></child-component>
</div>
</template>
<script>
export default {
name: 'PassingPropsSample'
inheritAttrs: false,
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>
- Higher Order Components in Vue.js
- Do we need Higher Order Components in Vue.js?
- Higher-Order Components in Vue.js
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>
</parent-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
.
- Official API
- Official Guide
- Component Communication
- Dependency Injection in Vue.js App with TypeScript
// 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>
// 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>
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>
watch on create
// don't
created() {
this.fetchUserList();
},
watch: {
searchText: 'fetchUserList',
}
// do
watch: {
searchText: {
handler: 'fetchUserList',
immediate: true,
}
}
- 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
- 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?
- 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
- 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
- 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
- [Video] - VueNYC - Three Vue code smells, and what you can do about them - Matt Rothenberg (@mattrothenberg)
- 81: Evan You - Advanced Vue Component Design
- 7 Secret Patterns Vue Consultants Don’t Want You to Know
- Vue + TypeScript: A Match Made in Your Code Editor
- Writing Class-Based Components with Vue.js and TypeScript
- 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
- 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