惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
F
Fortinet All Blogs
B
Blog
GbyAI
GbyAI
P
Proofpoint News Feed
量子位
The Register - Security
The Register - Security
宝玉的分享
宝玉的分享
大猫的无限游戏
大猫的无限游戏
云风的 BLOG
云风的 BLOG
V
Visual Studio Blog
B
Blog RSS Feed
WordPress大学
WordPress大学
Recorded Future
Recorded Future
Recent Announcements
Recent Announcements
V
Vulnerabilities – Threatpost
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Secure Thoughts
雷峰网
雷峰网
Stack Overflow Blog
Stack Overflow Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Webroot Blog
Webroot Blog
AWS News Blog
AWS News Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The GitHub Blog
The GitHub Blog
爱范儿
爱范儿
O
OpenAI News
月光博客
月光博客
H
Hacker News: Front Page
S
Security Affairs
W
WeLiveSecurity
The Hacker News
The Hacker News
aimingoo的专栏
aimingoo的专栏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Help Net Security
Help Net Security
MongoDB | Blog
MongoDB | Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
D
Docker
T
The Blog of Author Tim Ferriss
Spread Privacy
Spread Privacy
Blog — PlanetScale
Blog — PlanetScale
J
Java Code Geeks
S
Securelist
Microsoft Azure Blog
Microsoft Azure Blog
TaoSecurity Blog
TaoSecurity Blog
T
Threat Research - Cisco Blogs
M
MIT News - Artificial intelligence
A
About on SuperTechFans

Supabase Blog

AI Agents Know About Supabase. They Don't Always Use It Right. Custom OIDC Providers for Supabase Auth 100,000 GitHub stars Supabase docs over SSH Navigating Regional Network Blocks Supabase Joins the Stripe Projects Developer Preview Log Drains: Now available on Pro Supabase Storage: major performance, security, and reliability updates Supabase incident on February 12, 2026 Hydra joins Supabase X / Twitter OAuth 2.0 is now available for Supabase Auth BKND joins Supabase Supabase is now an official Claude connector Supabase PrivateLink is now available Introducing: Postgres Best Practices When to use Read Replicas vs. bigger compute Introducing TRAE SOLO integration with Supabase Supabase Security Retro: 2025 Sync Stripe Data to Your Supabase Database in One Click Building ChatGPT Apps with Supabase Edge Functions and mcp-use Own Your Observability: Supabase Metrics API Introducing iceberg-js: A JavaScript Client for Apache Iceberg Introducing Supabase for Platforms Adding Async Streaming to Postgres Foreign Data Wrappers Build "Sign in with Your App" using Supabase Auth Introducing Seven New Email Templates for Supabase Auth The new Supabase power for Kiro Introducing Supabase ETL Introducing Analytics Buckets Introducing Vector Buckets Snap, Inc. Launches Snap Cloud, Powered by Supabase Triplit joins Supabase Supabase Series E 1000 Y Combinator Founders Choose Supabase gm 👋 web3, welcome aboard to Sign in with Web3 (Solana, Ethereum) Announcing the Supabase Remote MCP Server Enterprise speed, enterprise standards with Bolt Cloud + Supabase PostgREST 13 Lovable Cloud + Supabase: The Default Platform for AI Builders Processing large jobs with Edge Functions, Cron, and Queues Defense in Depth for MCP Servers OrioleDB Patent: now freely available to the Postgres community Supabase Launch Week 15 Hackathon Winner Announcement The Vibe Coder's Guide to Supabase Environments Testing for Vibe Coders: From Zero to Production Confidence The Vibe Coding Master Checklist Vibe Coding: Best Practices for Prompting Supabase Auth: Build vs. Buy Top 10 Launches of Launch Week 15 Supabase Launch Week 15 Hackathon Storage: 10x Larger Uploads, 3x Cheaper Cached Egress, and 2x Egress Quota Persistent Storage and 97% Faster Cold Starts for Edge Functions Algolia Connector for Supabase New Observability Features in Supabase Improved Security Controls and A New Home for Security Introducing Branching 2.0 Stripe-To-Postgres Sync Engine as standalone Library Supabase Analytics Buckets with Iceberg Support Create a Supabase backend using Figma Make Introducing JWT Signing Keys Supabase UI: Platform Kit Build a Personalized AI Assistant with Postgres Announcing Multigres: Vitess for Postgres Building on open table formats Open Data Standards: Postgres, OTel, and Iceberg Simplifying back-end complexity with Supabase Data APIs PostgreSQL Event Triggers without superuser access Top 10 Launches of Launch Week 14 Supabase MCP Server Data API Routes to Nearest Read Replica Declarative Schemas for Simpler Database Management Realtime: Broadcast from Database Keeping Tabs on What's New in Supabase Studio Edge Functions: Deploy from the Dashboard + Deno 2.1 Automatic Embeddings in Postgres Introducing the Supabase UI Library Supabase Auth: Bring Your Own Clerk Postgres Language Server: Initial Release Migrating from Fauna to Supabase Migrating from the MongoDB Data API to Supabase Dedicated Poolers Postgres as a Graph Database: (Ab)using pgRouting AI Hackathon at Y Combinator Calendars in Postgres using Foreign Data Wrappers Supabase Launch Week 13 Hackathon Winners How to Hack the Base! Running Durable Workflows in Postgres using DBOS database.build v2: Bring-your-own-LLM Restore to a New Project Hack the Base! with Supabase Top 10 Launches of Launch Week 13 Supabase Queues High Performance Disk Supabase Cron Supabase CLI v2: Config as Code Supabase Edge Functions: Introducing Background Tasks, Ephemeral Storage, and WebSockets Supabase AI Assistant v2 OrioleDB Public Alpha Executing Dynamic JavaScript Code on Supabase with Edge Functions ClickHouse Partnership, improved Postgres Replication, and Disk Management
Infinite scroll with Next.js, Framer Motion, and Supabase
Francesco Sansalvadore · 2023-04-04 · via Supabase Blog

Infinite scroll with Next.js, Framer Motion, and Supabase

Imagine you generated a bunch of tickets for the Supabase Launch Week and you wanted to display them all on one page. It might take a while to load them all in one go, so a great solution to this would be to lazy load smaller subsets while the user scrolls, with a technique called infinite scrolling.

An infinite scroll allows you to scroll through your content endlessly while only loading the data you need as you need it. This improves performance while also creating a smooth and delightful user experience.

In this post, we'll go through the steps of how to create an infinite scroll using NextJs, Supabase, and Framer Motion.

Infinite scroll is a web design technique that automatically loads and displays new content as a user scrolls down a web page. This creates an endless scrolling experience that eliminates the need for pagination and provides a seamless and uninterrupted browsing experience.

Infinite scroll can enhance user experience and engagement by providing a fluid and intuitive browsing experience. It also allows for faster content delivery. By dynamically loading and displaying content as needed, infinite scroll can reduce the time and effort required to load pages and deliver content, improving overall performance and user satisfaction.

Next.js is a great choice for implementing infinite scroll in a web application. It provides server-side rendering and automatic code splitting, which can improve performance and optimize the loading of content as the user scrolls down the page. It also includes built-in support for dynamic imports and lazy loading, which can be useful for fetching and displaying content as needed.

Plus, incremental static regeneration (ISR) and pre-fetching of data can further improve performance by enabling the server to generate and cache pages in advance, and to pre-fetch data as the user scrolls down the page.

Now, onto the code!

First things first, let's install the dependencies we'll need.


_10

npm install @supabase/supabase-js lodash framer-motion


Set up the supabase-js client and fetch the first 20 tickets through getServerSideProps so we don't start with an empty screen.

We'll assume our table in the db is called my_tickets_table.


_29

import { useEffect, useState } from 'react'

_29

import { createClient } from '@supabase/supabase-js'

_29

_29

const supabase = createClient('supabase-url', 'supabase-key')

_29

_29

export default function TicketsPage({ tickets }) {

_29

const [loadedTickets, setLoadedTickets] = useState(tickets)

_29

_29

return (

_29

<div>

_29

{loadedTickets.map((ticket, index) => (

_29

{/* We'll get to this part later */}

_29

))}

_29

</div>

_29

)

_29

_29

export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {

_29

const { data: tickets } = await supabase!

_29

.from('my_tickets_table')

_29

.select('*')

_29

.order('createdAt', { ascending: false })

_29

.limit(20)

_29

_29

return {

_29

props: {

_29

tickets,

_29

},

_29

}

_29

}


To detect if the user is scrolling we can listen to the window scroll event.

If you try to console.log() the event returned in the listener callback, you'll notice that it fires on every single pixel scrolled. We want to avoid triggering like crazy, so we'll use a lodash debounce function to limit how often we call this event to once every 200 milliseconds.

Here's what it looks like:


_14

import { useEffect } from 'react'

_14

import { debounce } from 'lodash'

_14

_14

const handleScroll = () => {

_14

// Do stuff when scrolling

_14

}

_14

_14

useEffect(() => {

_14

const handleDebouncedScroll = debounce(() => handleScroll(), 200)

_14

window.addEventListener('scroll', handleDebouncedScroll)

_14

return () => {

_14

window.removeEventListener('scroll', handleDebouncedScroll)

_14

}

_14

}, [])


Next, we want to check if the bottom of the tickets container intersects with the bottom of the viewport. We can use the getBoundingClientRect method to get the position of the container and then compare it with the height of the viewport.


_24

import { useRef, useState } from 'react'

_24

_24

// ...

_24

_24

const containerRef = useRef(null)

_24

const [offset, setOffset] = useState(1)

_24

const [isInView, setIsInView] = useState(false)

_24

_24

const handleScroll = (container) => {

_24

if (containerRef.current && typeof window !== 'undefined') {

_24

const container = containerRef.current

_24

const { bottom } = container.getBoundingClientRect()

_24

const { innerHeight } = window

_24

setIsInView((prev) => bottom <= innerHeight)

_24

}

_24

}

_24

_24

useEffect(() => {

_24

if (isInView) {

_24

loadMoreUsers(offset)

_24

}

_24

}, [isInView])

_24

_24

return <div ref={containerRef}>{/* List of loaded tickets */}</div>


Now we can load more tickets based on the current offset. We'll use the range method from the supabase-js library to easily work with the pagination logic.


_37

export default function TicketsPage() {

_37

const PAGE_COUNT = 20

_37

const [offset, setOffset] = useState(1)

_37

const [isLoading, setIsLoading] = useState(false)

_37

const [isInView, setIsInView] = useState(false)

_37

_37

useEffect(() => {

_37

if (isInView) {

_37

loadMoreTickets(offset)

_37

}

_37

}, [isInView])

_37

_37

const loadMoreTickets = async (offset: number) => {

_37

setIsLoading(true)

_37

// Every time we fetch, we want to increase

_37

// the offset to load fresh tickets

_37

setOffset((prev) => prev + 1)

_37

const { data: newTickets } = await fetchTickets(offset, PAGE_COUNT)

_37

// Merge new tickets with all previously loaded

_37

setLoadedTickets((prevTickets) => [...prevTickets, ...newTickets])

_37

setIsLoading(false)

_37

}

_37

_37

const fetchTickets = async (offset, limit) => {

_37

const from = offset * PAGE_COUNT

_37

const to = from + PAGE_COUNT - 1

_37

_37

const { data } = await supabase!

_37

.from('my_tickets_table')

_37

.select('*')

_37

.range(from, to)

_37

.order('createdAt', { ascending: false })

_37

_37

_37

return data

_37

}

_37

}


Now that we have our tickets loaded, we want to add some animation to make them pop like dominos as they appear on the screen. For this, we're going to use the Framer Motion library.

We'll wrap each ticket in a motion component and add a transition effect to stagger their appearance on the screen:


_26

import { motion } from 'framer-motion'

_26

_26

// ...

_26

_26

{

_26

loadedTickets.map((ticket, index) => {

_26

// each ticket will be delayed based on it's index

_26

// but we need to subtract the delay from all the previously loaded tickets

_26

const recalculatedDelay = i >= PAGE_COUNT * 2 ? (i - PAGE_COUNT * (offset - 1)) / 15 : i / 15

_26

_26

return (

_26

<motion.div

_26

key={ticket.id}

_26

initial={{ opacity: 0, y: 20 }}

_26

animate={{ opacity: 1, y: 0 }}

_26

transition={{

_26

duration: 0.4,

_26

ease: [0.25, 0.25, 0, 1],

_26

delay: recalculatedDelay,

_26

}}

_26

>

_26

{/* Actual ticket component */}

_26

</motion.div>

_26

)

_26

})

_26

}


With this code, each ticket will start with an opacity of 0 and a y position of 20. As it animates into view, it will fade in and move up to its final position. The delay for each ticket will be based on its index in the array, creating a nice staggered effect.

Once all the tickets have been loaded, we want to stop listening to the scroll event to avoid unnecessary requests. We can do this by setting a state variable called isLast to true whenever the length of the response will be less than PAGE_COUNT:


_10

if (newTickets.length < PAGE_COUNT) {

_10

setIsLast(true)

_10

}


We'll use this code to conditionally remove the event listener:


_10

useEffect(() => {

_10

const handleDebouncedScroll = debounce(() => !isLast && handleScroll(), 200)

_10

window.addEventListener('scroll', handleScroll)

_10

return () => {

_10

window.removeEventListener('scroll', handleScroll)

_10

}

_10

}, [])


Now, when the isLast state variable is true, the event listener will be removed and the component will stop listening to the scroll event.

That's it! We hope this post enabled you to build the next awesome infinite scroll.

Here's the complete code:


_98

import { useEffect, useState, useRef } from 'react'

_98

import { createClient } from '@supabase/supabase-js'

_98

import { debounce } from 'lodash'

_98

import { motion } from 'framer-motion'

_98

_98

const supabase = createClient('supabase-url', 'supabase-key')

_98

_98

export default function TicketsPage({ tickets }) {

_98

const PAGE_COUNT = 20

_98

const containerRef = useRef(null)

_98

const [loadedTickets, setLoadedTickets] = useState(tickets)

_98

const [offset, setOffset] = useState(1)

_98

const [isLoading, setIsLoading] = useState(false)

_98

const [isInView, setIsInView] = useState(false)

_98

_98

const handleScroll = (container) => {

_98

if (containerRef.current && typeof window !== 'undefined') {

_98

const container = containerRef.current

_98

const { bottom } = container.getBoundingClientRect()

_98

const { innerHeight } = window

_98

setIsInView((prev) => bottom <= innerHeight)

_98

}

_98

}

_98

_98

useEffect(() => {

_98

const handleDebouncedScroll = debounce(() => !isLast && handleScroll(), 200)

_98

window.addEventListener('scroll', handleScroll)

_98

return () => {

_98

window.removeEventListener('scroll', handleScroll)

_98

}

_98

}, [])

_98

_98

useEffect(() => {

_98

if (isInView) {

_98

loadMoreTickets(offset)

_98

}

_98

}, [isInView])

_98

_98

const loadMoreTickets = async (offset: number) => {

_98

setIsLoading(true)

_98

setOffset((prev) => prev + 1)

_98

const { data: newTickets } = await fetchTickets(offset, PAGE_COUNT)

_98

setLoadedTickets((prevTickets) => [...prevTickets, ...newTickets])

_98

setIsLoading(false)

_98

}

_98

_98

const fetchTickets = async (offset) => {

_98

const from = offset * PAGE_COUNT

_98

const to = from + PAGE_COUNT - 1

_98

_98

const { data } = await supabase!

_98

.from('my_tickets_table')

_98

.select('*')

_98

.range(from, to)

_98

.order('createdAt', { ascending: false })

_98

_98

return data

_98

}

_98

_98

return (

_98

<div ref={containerRef}>

_98

{

_98

loadedTickets.map((ticket, index) => {

_98

const recalculatedDelay =

_98

i >= PAGE_COUNT * 2 ? (i - PAGE_COUNT * (offset - 1)) / 15 : i / 15

_98

_98

return (

_98

<motion.div

_98

key={ticket.id}

_98

initial={{ opacity: 0, y: 20 }}

_98

animate={{ opacity: 1, y: 0 }}

_98

transition={{

_98

duration: 0.4,

_98

ease: [0.25, 0.25, 0, 1],

_98

delay: recalculatedDelay,

_98

}}

_98

>

_98

{/* Actual ticket component */}

_98

</motion.div>

_98

)

_98

})

_98

}

_98

</div>

_98

)

_98

_98

export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {

_98

const { data: tickets } = await supabase!

_98

.from('my_tickets_table')

_98

.select('*')

_98

.order('createdAt', { ascending: false })

_98

.limit(20)

_98

_98

return {

_98

props: {

_98

tickets,

_98

},

_98

}

_98

}


If you also want to feature in the endless tickets page and have a chance to win cool swag, you're invited to generate your unique ticket until April 16th.

See you at Launch Week! 👋