Learn how to easily add a wish list feature to your web app and boost user engagement with our step-by-step guide.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Why Wish Lists Matter for Your Business
A well-implemented wish list feature does more than just let users bookmark items—it's a powerful conversion tool that provides valuable data about customer desires while reducing cart abandonment. When users can save items for later instead of abandoning them entirely, you maintain a connection to potential purchases that might otherwise be lost.
1. Data Structure Considerations
At its core, a wish list is a many-to-many relationship between users and products. Let's break down what this means in database terms:
-- Basic wishlist table structure
CREATE TABLE wishlists (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
name VARCHAR(255) DEFAULT 'My Wishlist', -- Allow users to name their lists
is_public BOOLEAN DEFAULT FALSE, -- Privacy controls
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE wishlist_items (
wishlist_id INT NOT NULL,
product_id INT NOT NULL,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (wishlist_id, product_id), // Composite key prevents duplicates
FOREIGN KEY (wishlist_id) REFERENCES wishlists(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);
This structure supports multiple wish lists per user and includes timestamps for analytics purposes.
2. Backend Implementation
Your API endpoints should handle the core CRUD operations. Here's a RESTful approach using Express.js:
// routes/wishlist.js
const express = require('express');
const router = express.Router();
const auth = require('../middleware/auth');
// Get all wishlists for the current user
router.get('/', auth, async (req, res) => {
try {
const wishlists = await db.query(
'SELECT * FROM wishlists WHERE user_id = ?',
[req.user.id]
);
res.json(wishlists);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Add item to wishlist
router.post('/:wishlistId/items', auth, async (req, res) => {
const { wishlistId } = req.params;
const { productId } = req.body;
try {
// First verify the wishlist belongs to the user
const wishlist = await db.query(
'SELECT * FROM wishlists WHERE id = ? AND user_id = ?',
[wishlistId, req.user.id]
);
if (wishlist.length === 0) {
return res.status(404).json({ message: 'Wishlist not found' });
}
// Add item to wishlist
await db.query(
'INSERT INTO wishlist_items (wishlist_id, product_id) VALUES (?, ?)',
[wishlistId, productId]
);
res.status(201).json({ message: 'Item added to wishlist' });
} catch (err) {
// Handle duplicate entries gracefully
if (err.code === 'ER_DUP_ENTRY') {
return res.status(400).json({ message: 'Item already in wishlist' });
}
res.status(500).json({ message: err.message });
}
});
// Remove item from wishlist
router.delete('/:wishlistId/items/:productId', auth, async (req, res) => {
// Similar implementation with proper authorization checks
});
module.exports = router;
3. Frontend Implementation
The UI component needs to be both intuitive and visually consistent with your brand. Here's a React implementation:
// WishlistButton.jsx
import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { HeartIcon, HeartFilledIcon } from '../components/Icons';
const WishlistButton = ({ productId }) => {
const { user, isAuthenticated } = useAuth();
const [isInWishlist, setIsInWishlist] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
// Check if item is in wishlist when component mounts
if (isAuthenticated) {
checkWishlistStatus();
}
}, [productId, isAuthenticated]);
const checkWishlistStatus = async () => {
try {
const response = await fetch(`/api/wishlists/default/items/${productId}`);
setIsInWishlist(response.ok);
} catch (error) {
console.error('Error checking wishlist status:', error);
}
};
const toggleWishlist = async () => {
if (!isAuthenticated) {
// Prompt for login or save to local storage
return window.location.href = '/login?redirect=' + window.location.pathname;
}
setIsLoading(true);
try {
if (isInWishlist) {
// Remove from wishlist
await fetch(`/api/wishlists/default/items/${productId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' }
});
} else {
// Add to wishlist
await fetch(`/api/wishlists/default/items`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId })
});
}
setIsInWishlist(!isInWishlist);
} catch (error) {
console.error('Error updating wishlist:', error);
} finally {
setIsLoading(false);
}
};
return (
<button
onClick={toggleWishlist}
disabled={isLoading}
className={`wishlist-btn ${isInWishlist ? 'active' : ''}`}
aria-label={isInWishlist ? "Remove from wishlist" : "Add to wishlist"}
>
{isInWishlist ? <HeartFilledIcon /> : <HeartIcon />}
</button>
);
};
export default WishlistButton;
Handling Anonymous Users
Don't lose potential data just because users aren't logged in. Implement local storage for guest wish lists:
// wishlistService.js
export const addToWishlist = (productId) => {
if (isAuthenticated()) {
// API call to backend
return apiAddToWishlist(productId);
} else {
// Store in localStorage
const wishlist = JSON.parse(localStorage.getItem('guestWishlist') || '[]');
if (!wishlist.includes(productId)) {
wishlist.push(productId);
localStorage.setItem('guestWishlist', JSON.stringify(wishlist));
}
return Promise.resolve();
}
};
// During user login, migrate local wishlist to database
export const migrateGuestWishlist = async () => {
const guestWishlist = JSON.parse(localStorage.getItem('guestWishlist') || '[]');
if (guestWishlist.length > 0) {
// Add items to user's wishlist via API
await Promise.all(guestWishlist.map(productId => apiAddToWishlist(productId)));
// Clear local storage
localStorage.removeItem('guestWishlist');
}
};
Price Drop Notifications
One of the most powerful ways to convert wish list items to sales is with price alerts:
// In your product price update hook or job
const notifyPriceDrops = async (product) => {
// Find all users who have this product in their wishlist
const users = await db.query(`
SELECT DISTINCT u.id, u.email, u.notification_preferences
FROM users u
JOIN wishlists w ON u.id = w.user_id
JOIN wishlist_items wi ON w.id = wi.wishlist_id
WHERE wi.product_id = ? AND u.notification_preferences->'$.price_alerts' = true
`, [product.id]);
// Send notifications
users.forEach(user => {
notificationService.sendPriceDropAlert({
userId: user.id,
email: user.email,
product: {
id: product.id,
name: product.name,
oldPrice: product.previous_price,
newPrice: product.current_price,
imageUrl: product.image_url,
url: `${process.env.SITE_URL}/products/${product.slug}`
}
});
});
};
Social Sharing
Enable users to share wish lists for special occasions:
// ShareWishlistModal.jsx
const ShareWishlistModal = ({ wishlistId, isOpen, onClose }) => {
const [shareUrl, setShareUrl] = useState('');
const [isPublic, setIsPublic] = useState(false);
useEffect(() => {
// Fetch wishlist details including public status
const fetchWishlistDetails = async () => {
const response = await fetch(`/api/wishlists/${wishlistId}`);
const data = await response.json();
setIsPublic(data.is_public);
if (data.is_public) {
setShareUrl(`${window.location.origin}/wishlists/shared/${wishlistId}`);
}
};
if (isOpen && wishlistId) {
fetchWishlistDetails();
}
}, [isOpen, wishlistId]);
const togglePublicStatus = async () => {
// Toggle wishlist privacy setting
const response = await fetch(`/api/wishlists/${wishlistId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_public: !isPublic })
});
if (response.ok) {
const newStatus = !isPublic;
setIsPublic(newStatus);
if (newStatus) {
setShareUrl(`${window.location.origin}/wishlists/shared/${wishlistId}`);
} else {
setShareUrl('');
}
}
};
return (
<Modal isOpen={isOpen} onClose={onClose}>
<h3>Share Your Wishlist</h3>
<div className="privacy-toggle">
<label>
<input
type="checkbox"
checked={isPublic}
onChange={togglePublicStatus}
/>
Make this wishlist public
</label>
</div>
{isPublic && (
<div className="share-options">
<input
type="text"
value={shareUrl}
readOnly
onClick={(e) => e.target.select()}
/>
<div className="social-buttons">
<button onClick={() => window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`)}>
Share on Facebook
</button>
<button onClick={() => window.open(`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=Check out my wishlist!`)}>
Share on Twitter
</button>
<button onClick={() => navigator.clipboard.writeText(shareUrl)}>
Copy Link
</button>
</div>
</div>
)}
</Modal>
);
};
Caching Strategy
Wish lists are read frequently but updated less often, making them perfect for caching:
// middleware/wishlistCache.js
const redis = require('redis');
const client = redis.createClient(process.env.REDIS_URL);
const CACHE_TTL = 3600; // 1 hour in seconds
const wishlistCache = async (req, res, next) => {
// Only cache GET requests
if (req.method !== 'GET') return next();
const userId = req.user.id;
const cacheKey = `wishlist:${userId}`;
try {
// Try to get from cache
client.get(cacheKey, (err, data) => {
if (err) throw err;
if (data) {
// Cache hit
return res.json(JSON.parse(data));
}
// Cache miss - store original res.json method
const originalJson = res.json;
// Override res.json method to cache response before sending
res.json = function(body) {
client.setex(cacheKey, CACHE_TTL, JSON.stringify(body));
return originalJson.call(this, body);
};
next();
});
} catch (err) {
// If caching fails, just continue without caching
console.error('Cache error:', err);
next();
}
};
// Invalidate cache when wishlist is modified
const invalidateWishlistCache = (userId) => {
const cacheKey = `wishlist:${userId}`;
client.del(cacheKey);
};
module.exports = { wishlistCache, invalidateWishlistCache };
Database Indexing
Ensure your wishlist queries perform well with proper indexes:
-- Create indexes for frequent wishlist operations
CREATE INDEX idx_wishlists_user_id ON wishlists(user_id);
CREATE INDEX idx_wishlist_items_product_id ON wishlist_items(product_id);
Tracking Wish List Conversion Rates
Monitor how wish lists affect your conversion funnel:
// In your analytics service
const trackWishlistEvent = (eventType, data) => {
switch (eventType) {
case 'add_to_wishlist':
analytics.track('Add to Wishlist', {
product_id: data.productId,
product_name: data.productName,
product_price: data.price,
category: data.category,
wishlist_id: data.wishlistId,
source: data.source // 'product_page', 'category_page', etc.
});
break;
case 'remove_from_wishlist':
analytics.track('Remove from Wishlist', {
product_id: data.productId,
wishlist_id: data.wishlistId,
days_in_wishlist: data.daysInWishlist
});
break;
case 'add_to_cart_from_wishlist':
analytics.track('Add to Cart from Wishlist', {
product_id: data.productId,
wishlist_id: data.wishlistId,
days_in_wishlist: data.daysInWishlist
});
break;
}
};
Phased Approach
Common Pitfalls
Key Test Cases
// wishlist.test.js
describe('Wishlist Functionality', () => {
test('Should add item to wishlist when authenticated', async () => {
// Setup test user and authentication
const user = await createTestUser();
const token = generateAuthToken(user);
// Test API endpoint
const response = await request(app)
.post('/api/wishlists/1/items')
.set('Authorization', `Bearer ${token}`)
.send({ productId: 123 });
expect(response.statusCode).toBe(201);
// Verify database state
const wishlistItem = await db.query(
'SELECT * FROM wishlist_items WHERE wishlist_id = ? AND product_id = ?',
[1, 123]
);
expect(wishlistItem.length).toBe(1);
});
test('Should store wishlist items in localStorage for guest users', async () => {
// Setup test environment
localStorage.clear();
// Execute the function
await addToWishlist(456);
// Verify localStorage state
const wishlist = JSON.parse(localStorage.getItem('guestWishlist'));
expect(wishlist).toContain(456);
});
test('Should migrate guest wishlist to user wishlist after login', async () => {
// Setup
localStorage.setItem('guestWishlist', JSON.stringify([789, 101]));
const user = await createTestUser();
// Simulate login and migration
await loginUser(user);
await migrateGuestWishlist();
// Verify migration
const userWishlistItems = await db.query(
'SELECT product_id FROM wishlist_items JOIN wishlists ON wishlist_items.wishlist_id = wishlists.id WHERE wishlists.user_id = ?',
[user.id]
);
expect(userWishlistItems.map(item => item.product_id)).toContain(789);
expect(userWishlistItems.map(item => item.product_id)).toContain(101);
expect(localStorage.getItem('guestWishlist')).toBeNull();
});
});
A well-implemented wish list feature sits at the intersection of user experience and business goals. By building a system that's persistent, performant, and properly integrated with your analytics, you create more than just a bookmark feature—you establish a conversion pipeline that turns browsing into buying.
Remember that the best wish list implementations adapt to how your specific customers shop. For some businesses, multiple lists with organization features make sense; for others, a simple save-for-later function is sufficient. Let your users' behavior guide your implementation priorities while ensuring the technical foundation remains solid.
Explore the top 3 wish list use cases to boost user engagement and sales in your web app.
A wish list creates an intentional "digital waiting room" for future purchases, keeping customers engaged with your brand between transactions and providing valuable insights into consumer desires before they're ready to buy.
When implemented with social sharing capabilities, wish lists transform private consumer intentions into public endorsements, extending your product visibility beyond traditional marketing channels.
Wish lists provide explicit preference data that significantly enhances recommendation algorithms, allowing for more sophisticated personalization than browsing or purchase history alone can provide.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â