I'm using Alpine.js to render a list of videos from an API. After making the API call, the response data populates my videos array successfully, but the page doesn’t update to show the new data.
Here's a breakdown of what’s happening:
I'm making an Axios API request and pushing the resulting videos to the videos array. I’ve confirmed via console log that videos is populated with the correct data. Despite this, the template does not render the updated array.
Thank you!
<div x-data="app" class="flex flex-wrap justify-between">
<template x-for="video in videos" :key="video.id">
<!--video content-->
<div>Test Video</div>
</template>
</div>
const app = {
triggerElement: null,
page: 1,
lastPage: 24,
itemsPerPage: 24,
observer: null,
isObserverPolyfilled: false,
videos: [],
debug: true,
loaded: false,
init: function () {
window.addEventListener('urlChange', () => {
app.getItems();
});
app.triggerElement = document.querySelector('#infinite-scroll-trigger');
document.addEventListener('DOMContentLoaded', function () {
// app.getItems();
app.loaded = true;
});
app.infiniteScroll();
(app.debug) ? console.log('init' ) : '';
},
infiniteScroll: function () {
(app.debug) ? console.log('infiniteScroll' ) : '';
// Check if browser can use IntersectionObserver which is waaaay more performant
if (!('IntersectionObserver' in window) ||
!('IntersectionObserverEntry' in window) ||
!('isIntersecting' in window.IntersectionObserverEntry.prototype) ||
!('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
// Loading polyfill since IntersectionObserver is not available
this.isObserverPolyfilled = true
// Storing function in window so we can wipe it when reached last page
window.alpineInfiniteScroll = {
scrollFunc() {
var position = app.triggerElement.getBoundingClientRect()
if (position.top < window.innerHeight && position.bottom >= 0) {
if (app.loaded) {
(app.debug) ? console.log('getItems 1' ) : '';
app.getItems();
}
}
}
}
window.addEventListener('scroll', window.alpineInfiniteScroll.scrollFunc)
} else {
// We can access IntersectionObserver
this.observer = new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting === true) {
if (app.loaded) {
(app.debug) ? console.log('getItems 2' ) : '';
app.getItems();
}
}
}, {threshold: [0]})
this.observer.observe(this.triggerElement)
}
},
getItems: function () {
// TODO: Do fetch here for the content and concat it to populated items
// TODO: Set last page from API call - ceil it
let currentURL = new URL(window.location.href);
currentURL.hostname = 'api.' + currentURL.hostname;
axios.post(currentURL, {
page: this.page,
perPage: this.itemsPerPage,
})
.then(response => {
const data = response.data;
// this.lastPage = data.total_pages;
app.videos = data.data.videos;
// this.page++;
(app.debug) ? console.log(response) : '';
})
.catch(error => {
console.error('Error loading videos:', error);
(app.debug) ? console.log(error) : '';
this.loading = false;
});
// Next page
this.page++
// We have shown the last page - clean up
if (this.lastPage && this.page > this.lastPage) {
if (this.isObserverPolyfilled) {
window.removeEventListener('scroll', window.alpineInfiniteScroll.scrollFunc)
(app.debug) ? console.log('alpineInfiniteScroll') : '';
} else if (this.observer && this.triggerElement) {
try {
this.observer.unobserve(this.triggerElement);
} catch (e) {
console.error('Failed to unobserve element:', e);
}
}
if (this.triggerElement && this.triggerElement.parentNode) {
this.triggerElement.parentNode.removeChild(this.triggerElement);
this.triggerElement = null; // Prevent further access to the removed element
}
}
}
};
this.$nextTick()
after updating videos to ensure Alpine.js updates.None of these approaches resolved the issue. I'd appreciate any insight into why Alpine.js might not be re-rendering the updated data or any alternative solutions.
I tried to reproduce your issue on my local machine and ended up writing a fully working example. I hope it will help you to compare with your actual codebase and see what's wrong:
index.html
Make sure you wrap everything into div
with x-data
and x-init
.
You will also notice I've created a function that returns an app object with some changes to your code as follows in js example below. By defining init within your component and calling it via x-init, you ensure that your setup code runs when the component is initialized.
<div x-data="app()" x-init="init">
<!-- Video List -->
<div id="video-list">
<template x-for="video in videos" :key="video.id">
<div class="video">
<h3 x-text="video.title"></h3>
<p x-text="video.description"></p>
</div>
</template>
</div>
<!-- Infinite Scroll Trigger -->
<div id="infinite-scroll-trigger"></div>
</div>
app.js
function app() {
return {
// Data properties
triggerElement: null,
page: 1,
lastPage: null,
itemsPerPage: 10,
observer: null,
isObserverPolyfilled: false,
videos: [],
debug: true,
loading: false,
// Initialization method
init() {
this.triggerElement = document.querySelector("#infinite-scroll-trigger");
this.infiniteScroll();
if (this.debug) console.log("Initialization complete.");
},
// Method to set up infinite scroll
infiniteScroll() {
if (this.debug) console.log("Setting up infinite scroll.");
const supportsIntersectionObserver = "IntersectionObserver" in window;
if (supportsIntersectionObserver) {
// Use IntersectionObserver for better performance
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{
threshold: 0,
}
);
this.observer.observe(this.triggerElement);
} else {
// Fallback for browsers without IntersectionObserver support
this.isObserverPolyfilled = true;
this.scrollFunc = this.handleScroll.bind(this);
window.addEventListener("scroll", this.scrollFunc);
}
},
// Handler for IntersectionObserver
handleIntersection(entries) {
if (entries[0].isIntersecting && !this.loading) {
if (this.debug)
console.log("Trigger element intersected. Loading items...");
this.getItems();
}
},
// Handler for scroll event (polyfill)
handleScroll() {
const position = this.triggerElement.getBoundingClientRect();
if (
position.top < window.innerHeight &&
position.bottom >= 0 &&
!this.loading
) {
if (this.debug)
console.log("Trigger element visible. Loading items...");
this.getItems();
}
},
// Method to fetch items from the mock API
async getItems() {
if (this.loading) return; // Prevent multiple calls
this.loading = true;
try {
const response = await axios.get("http://localhost:3000/videos", {
params: {
_page: this.page,
_limit: this.itemsPerPage,
},
});
const totalItems = response.headers["x-total-count"];
this.lastPage = Math.ceil(totalItems / this.itemsPerPage);
this.videos = this.videos.concat(response.data);
if (this.debug) console.log(`Loaded page ${this.page}.`, response.data);
this.page++;
// Check if we've reached the last page
if (this.page > this.lastPage) {
this.cleanup();
}
} catch (error) {
console.error("Error loading videos:", error);
} finally {
this.loading = false;
}
},
// Cleanup method to remove observers and event listeners
cleanup() {
if (this.isObserverPolyfilled && this.scrollFunc) {
window.removeEventListener("scroll", this.scrollFunc);
if (this.debug) console.log("Removed scroll event listener.");
}
if (this.observer) {
this.observer.disconnect();
if (this.debug) console.log("Disconnected IntersectionObserver.");
}
if (this.triggerElement) {
this.triggerElement.remove();
this.triggerElement = null;
if (this.debug) console.log("Removed trigger element.");
}
},
};
}
Key updates to JavaScript file:
this
statement instead of app
. Inside your Alpine component’s methods, this refers to the component’s data object. Using this, you can access and modify reactive properties.