Vue.js Higher Order Components
Introduction
Higher Order Components (HOCs) represent an advanced pattern in Vue.js that can significantly enhance your component architecture.
A Higher Order Component is essentially a function that takes a component as an argument and returns a new enhanced component. This pattern is borrowed from React and provides a powerful way to reuse component logic. While Vue has its own composition mechanisms like mixins and more recently the Composition API, understanding HOCs can give you another tool in your Vue.js development toolkit.
In this guide, we'll explore how HOCs work in Vue, when to use them, and how they compare to other composition patterns.
Understanding Higher Order Components
What is a HOC?
A Higher Order Component is a function that:
- Takes a component as an argument
- Returns a new component with enhanced functionality
- Doesn't modify the original component
Let's look at the basic structure:
// Basic HOC structure
const withSomething = (WrappedComponent) => {
return {
name: 'WithSomething',
props: WrappedComponent.props,
render(h) {
return h(WrappedComponent, {
props: this.$props,
// Pass additional props or override existing ones
});
}
}
}
How HOCs Compare to Other Vue Patterns
Here's how HOCs compare to other Vue patterns:
Creating Your First HOC
Let's create a simple HOC that adds a loading state to any component:
// withLoading.js
export const withLoading = (Component) => {
return {
name: 'WithLoading',
props: {
...Component.props,
isLoading: {
type: Boolean,
default: false
}
},
render(h) {
if (this.isLoading) {
return h('div', { class: 'loading-container' }, [
h('span', { class: 'loading-spinner' }, 'Loading...')
]);
}
return h(Component, {
props: this.$props,
on: this.$listeners,
scopedSlots: this.$scopedSlots,
});
}
};
};
Using the HOC
Now, let's use our HOC with a simple component:
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</div>
</template>
<script>
export default {
name: 'ArticleComponent',
props: {
title: String,
content: String
}
}
</script>
To enhance this component with our loading functionality:
import { withLoading } from './withLoading';
import ArticleComponent from './ArticleComponent.vue';
// Create enhanced component
const ArticleWithLoading = withLoading(ArticleComponent);
export default {
components: {
ArticleWithLoading
},
data() {
return {
isLoading: true,
articleTitle: 'Vue.js HOCs',
articleContent: 'This is an article about Higher Order Components.'
}
},
mounted() {
// Simulate loading delay
setTimeout(() => {
this.isLoading = false;
}, 2000);
},
template: `
<article-with-loading
:is-loading="isLoading"
:title="articleTitle"
:content="articleContent"
/>
`
}
In this example, when isLoading
is true, the loading spinner will be displayed. Once isLoading
becomes false, the original ArticleComponent will be rendered.
Practical Real-World Examples
Example 1: Authentication HOC
One common use case for HOCs is restricting access to components based on authentication:
// withAuth.js
export const withAuth = (Component, redirectPath = '/login') => {
return {
name: 'WithAuth',
props: Component.props,
beforeRouteEnter(to, from, next) {
const isAuthenticated = localStorage.getItem('auth_token');
if (!isAuthenticated) {
next(redirectPath);
} else {
next();
}
},
render(h) {
return h(Component, {
props: this.$props,
on: this.$listeners,
scopedSlots: this.$scopedSlots,
});
}
};
};
Usage:
import { withAuth } from './withAuth';
import UserDashboard from './UserDashboard.vue';
const SecureDashboard = withAuth(UserDashboard);
export default {
components: {
SecureDashboard
},
template: '<secure-dashboard />'
}
Example 2: Data Fetching HOC
Another common use case is wrapping components that need to fetch data:
// withFetch.js
import axios from 'axios';
export const withFetch = (Component, fetchOptions) => {
return {
name: 'WithFetch',
props: {
...Component.props,
fetchUrl: {
type: String,
required: true
}
},
data() {
return {
loading: true,
error: null,
data: null
};
},
created() {
this.fetchData();
},
watch: {
fetchUrl() {
this.fetchData();
}
},
methods: {
async fetchData() {
this.loading = true;
try {
const response = await axios.get(this.fetchUrl, fetchOptions);
this.data = response.data;
this.error = null;
} catch (err) {
this.error = err.message || 'An error occurred';
this.data = null;
} finally {
this.loading = false;
}
}
},
render(h) {
return h(Component, {
props: {
...this.$props,
data: this.data,
loading: this.loading,
error: this.error
},
on: this.$listeners,
scopedSlots: this.$scopedSlots,
});
}
};
};
Using the data fetching HOC:
<template>
<div>
<div v-if="loading">Loading users...</div>
<div v-else-if="error">Error: {{ error }}</div>
<ul v-else>
<li v-for="user in data" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>
<script>
import { withFetch } from './withFetch';
const UserList = {
props: ['data', 'loading', 'error']
};
export default withFetch(UserList);
</script>
And in a parent component:
<template>
<user-list-with-fetch fetch-url="https://api.example.com/users" />
</template>
<script>
import UserListWithFetch from './UserListWithFetch.vue';
export default {
components: { UserListWithFetch }
}
</script>
Advanced HOC Techniques
Composing Multiple HOCs
One of the powerful features of HOCs is that they can be composed together:
// Compose HOCs
const enhance = compose(
withAuth,
withLoading,
withFetch
);
// Usage
const EnhancedComponent = enhance(BaseComponent);
// A simple compose function
function compose(...funcs) {
if (funcs.length === 0) return arg => arg;
if (funcs.length === 1) return funcs[0];
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
HOC with Render Props
We can also create HOCs that leverage render props:
// withMousePosition.js
export const withMousePosition = (Component) => {
return {
name: 'WithMousePosition',
data() {
return {
x: 0,
y: 0
};
},
mounted() {
window.addEventListener('mousemove', this.updateMousePosition);
},
beforeDestroy() {
window.removeEventListener('mousemove', this.updateMousePosition);
},
methods: {
updateMousePosition(e) {
this.x = e.clientX;
this.y = e.clientY;
}
},
render(h) {
return h(Component, {
props: {
...this.$props,
mousePosition: {
x: this.x,
y: this.y
}
}
});
}
};
};
Usage:
<template>
<div>
Mouse is at: ({{ mousePosition.x }}, {{ mousePosition.y }})
</div>
</template>
<script>
import { withMousePosition } from './withMousePosition';
export default withMousePosition({
props: ['mousePosition']
});
</script>
Potential Drawbacks and Considerations
While HOCs are powerful, they come with some considerations:
-
Prop Naming Collisions: If multiple HOCs inject props with the same name, they can override each other.
-
Wrapper Component Hell: Overusing HOCs can lead to many nested wrapper components, complicating debugging.
-
Refs Don't Get Passed Through: Unlike in React, refs don't automatically get passed through HOCs in Vue.
-
Composition API Alternative: With Vue 3's Composition API, many use cases for HOCs can be handled more elegantly.
HOCs vs. Vue 3 Composition API
If you're working with Vue 3, the Composition API often provides a more elegant solution for logic reuse:
// Instead of withMousePosition HOC
export function useMousePosition() {
const x = ref(0);
const y = ref(0);
const updateMousePosition = (e) => {
x.value = e.clientX;
y.value = e.clientY;
};
onMounted(() => {
window.addEventListener('mousemove', updateMousePosition);
});
onBeforeUnmount(() => {
window.removeEventListener('mousemove', updateMousePosition);
});
return { x, y };
}
// Usage in a component
export default {
setup() {
const { x, y } = useMousePosition();
return { x, y };
}
}
Summary
Higher Order Components in Vue.js are a powerful pattern for component composition and logic reuse. While Vue provides other mechanisms like mixins, scoped slots, and the Composition API, HOCs still have their place in the Vue ecosystem—especially when you need to:
- Enhance components without modifying them
- Apply consistent behavior across multiple components
- Conditionally render components based on external factors
- Share complex functionality that affects rendering
In modern Vue development, especially with Vue 3, consider whether the Composition API might offer a cleaner solution for your specific use case. However, understanding HOCs expands your toolkit and gives you more options to architect your Vue applications effectively.
Additional Resources and Exercises
Resources
Exercises
-
Basic HOC Exercise: Create a
withTheme
HOC that provides theme variables to any component. -
Intermediate Exercise: Implement a
withLocalStorage
HOC that synchronizes component state with localStorage. -
Advanced Exercise: Create a set of composable HOCs for form handling that add validation, value persistence, and submission handling to any form component.
-
Comparison Exercise: Take an existing HOC you've created and reimplement its functionality using Vue 3's Composition API. Compare the two approaches.
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)