Vue.js Caching
In modern web development, optimizing HTTP requests is crucial for delivering a fast and responsive user experience. One powerful technique for improving performance is caching - storing data locally to avoid unnecessary network requests. In this tutorial, we'll explore various caching strategies for Vue.js applications and learn how to implement them effectively.
Introduction to Caching in Vue.js
Caching is the process of storing previously fetched data locally so that future requests for the same data can be served faster without making redundant network calls. This not only improves application performance but also reduces server load and bandwidth usage.
Vue.js applications typically fetch data via HTTP requests to APIs. Without proper caching, your app may:
- Make redundant API calls for the same data
- Experience slower page loads
- Create a poor user experience during network hiccups
Basic Caching with Local Variables
The simplest form of caching in Vue.js is using component-level or module-level variables to store API responses.
Example: Simple In-Memory Cache
// store.js
export const cache = {
data: {},
set(key, value, ttl = 300000) { // Default TTL: 5 minutes
this.data[key] = {
value,
expiry: ttl ? Date.now() + ttl : null
};
},
get(key) {
const item = this.data[key];
// Return null if item doesn't exist
if (!item) return null;
// Check if the item has expired
if (item.expiry && item.expiry < Date.now()) {
delete this.data[key];
return null;
}
return item.value;
}
};
Now you can use this simple cache in your Vue components:
<template>
<div>
<h2>User Profile</h2>
<div v-if="loading">Loading...</div>
<div v-else-if="user">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
<button @click="refreshData">Refresh Data</button>
</div>
</template>
<script>
import axios from 'axios';
import { cache } from './store';
export default {
data() {
return {
user: null,
loading: true
};
},
methods: {
async fetchUserData(id) {
this.loading = true;
// Check cache first
const cacheKey = `user-${id}`;
const cachedData = cache.get(cacheKey);
if (cachedData) {
this.user = cachedData;
this.loading = false;
console.log('Data loaded from cache');
return;
}
// If not in cache, fetch from API
try {
const response = await axios.get(`https://api.example.com/users/${id}`);
this.user = response.data;
// Store in cache for 5 minutes
cache.set(cacheKey, response.data, 5 * 60 * 1000);
console.log('Data loaded from API and cached');
} catch (error) {
console.error('Error fetching user data:', error);
} finally {
this.loading = false;
}
},
refreshData() {
// Force refresh from API by skipping cache
this.fetchUserData(1);
}
},
mounted() {
this.fetchUserData(1);
}
};
</script>
Advanced Caching with Vuex
For more complex applications, integrating caching with Vuex provides a centralized and reactive data store.
Example: Caching in Vuex Store
// store/modules/users.js
import axios from 'axios';
export default {
namespaced: true,
state: {
users: {},
cacheTimestamps: {},
cacheTTL: 5 * 60 * 1000 // 5 minutes in milliseconds
},
getters: {
getUserById: (state) => (id) => state.users[id],
isCacheValid: (state) => (id) => {
const timestamp = state.cacheTimestamps[id];
if (!timestamp) return false;
return Date.now() - timestamp < state.cacheTTL;
}
},
mutations: {
SET_USER(state, { id, user }) {
// Use Vue.set to ensure reactivity
Vue.set(state.users, id, user);
Vue.set(state.cacheTimestamps, id, Date.now());
},
CLEAR_CACHE(state) {
state.users = {};
state.cacheTimestamps = {};
}
},
actions: {
async fetchUser({ commit, state, getters }, { id, force = false }) {
// Return cached data if valid and not forced to refresh
if (!force && getters.isCacheValid(id) && state.users[id]) {
console.log(`Using cached data for user ID: ${id}`);
return state.users[id];
}
// Otherwise fetch from API
try {
console.log(`Fetching user ID: ${id} from API`);
const response = await axios.get(`https://api.example.com/users/${id}`);
commit('SET_USER', { id, user: response.data });
return response.data;
} catch (error) {
console.error(`Error fetching user ID: ${id}`, error);
throw error;
}
}
}
};
Then use it in your components:
<template>
<div>
<h2>User Details</h2>
<div v-if="loading">Loading user data...</div>
<div v-else-if="user">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
<button @click="refreshUser">Refresh</button>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
props: {
userId: {
type: [String, Number],
required: true
}
},
data() {
return {
loading: true
};
},
computed: {
...mapGetters('users', ['getUserById']),
user() {
return this.getUserById(this.userId);
}
},
methods: {
async loadUser(force = false) {
this.loading = true;
try {
await this.$store.dispatch('users/fetchUser', {
id: this.userId,
force
});
} catch (error) {
console.error('Failed to load user', error);
} finally {
this.loading = false;
}
},
refreshUser() {
this.loadUser(true); // Force refresh from API
}
},
created() {
this.loadUser();
},
watch: {
userId(newId) {
// Reload when userId changes
this.loadUser();
}
}
};
</script>
Implementing Cache Invalidation
Cache invalidation is determining when cached data is stale and should be refreshed. Here are some strategies:
1. Time-Based Invalidation (TTL)
This is the most common approach, where cached items expire after a set time.
function isExpired(timestamp, ttl) {
return Date.now() - timestamp > ttl;
}
2. Event-Based Invalidation
Invalidate cache based on specific events like user actions:
// In a component
methods: {
async updateUserProfile(userData) {
try {
await axios.put(`https://api.example.com/users/${userData.id}`, userData);
// Clear the cache for this user after update
this.$store.commit('users/INVALIDATE_USER', userData.id);
// Refresh the data
this.$store.dispatch('users/fetchUser', {
id: userData.id,
force: true
});
} catch (error) {
console.error('Failed to update profile', error);
}
}
}
Caching HTTP Requests with Axios
You can create a caching layer at the HTTP client level using Axios interceptors:
// axiosCache.js
import axios from 'axios';
const cache = new Map();
const cacheTTL = 5 * 60 * 1000; // 5 minutes
// Create a custom axios instance
const http = axios.create();
// Request interceptor
http.interceptors.request.use(config => {
// Skip cache for non-GET requests or if cache is disabled for this request
if (config.method !== 'get' || config.noCache) {
return config;
}
const cacheKey = `${config.url}${JSON.stringify(config.params || {})}`;
const cachedResponse = cache.get(cacheKey);
if (cachedResponse) {
const { timestamp, data } = cachedResponse;
// Check if cache is still valid
if (Date.now() - timestamp < cacheTTL) {
// Return cached response as a resolved Promise
return {
...config,
adapter: () => Promise.resolve({
data: data,
status: 200,
statusText: 'OK',
headers: {},
config,
request: {},
cached: true
})
};
}
}
return config;
}, error => {
return Promise.reject(error);
});
// Response interceptor
http.interceptors.response.use(response => {
// Don't cache if this response was already from cache
if (response.cached) {
return response;
}
// Only cache GET requests
if (response.config.method === 'get' && !response.config.noCache) {
const cacheKey = `${response.config.url}${JSON.stringify(response.config.params || {})}`;
// Store response in cache
cache.set(cacheKey, {
timestamp: Date.now(),
data: response.data
});
}
return response;
});
// Utility methods for cache management
export const clearCache = () => cache.clear();
export const removeFromCache = (url, params = {}) => {
const cacheKey = `${url}${JSON.stringify(params)}`;
cache.delete(cacheKey);
};
export default http;
Using this custom axios instance in components:
<template>
<div>
<h2>Posts</h2>
<button @click="loadPosts(true)">Refresh</button>
<div v-if="loading">Loading...</div>
<div v-else>
<div v-for="post in posts" :key="post.id" class="post">
<h3>{{ post.title }}</h3>
<p>{{ post.body }}</p>
</div>
</div>
</div>
</template>
<script>
import http, { clearCache } from './axiosCache';
export default {
data() {
return {
posts: [],
loading: true
};
},
methods: {
async loadPosts(forceRefresh = false) {
this.loading = true;
try {
const response = await http.get('https://api.example.com/posts', {
noCache: forceRefresh // Set to true to bypass cache
});
this.posts = response.data;
if (response.cached) {
console.log('Posts loaded from cache');
} else {
console.log('Posts loaded from API');
}
} catch (error) {
console.error('Error loading posts', error);
} finally {
this.loading = false;
}
},
clearAllCache() {
clearCache();
console.log('Cache cleared');
}
},
mounted() {
this.loadPosts();
}
};
</script>
Local Storage and IndexedDB Caching
For persistent caching across page reloads, you can use browser storage: