A reusable infinite scrolling component built using React, TanStack Query, and Tailwind CSS, with posts fetched from JSONPlaceholder.
- Reusable custom hook:
useInfinitePosts
- Auto-loads next posts when scrolling to the bottom
- Uses Intersection Observer under the hood
- Fully responsive & styled with Tailwind CSS
npm install @tanstack/react-query
npm install tailwindcss @tailwindcss/vite
Also make sure Tailwind is properly configured. You can use Tailwind CLI guide or Vite setup.
src/
├── api/
│ └── posts.js // API logic
├── hooks/
│ └── useInfinitePosts.js // Infinite query logic
├── components/
│ └── InfiniteScroll/
│ └── InfiniteScroll.jsx // UI logic
├── App.jsx
└── main.jsx
-
Set up TanStack Query Provider in
main.jsx
:import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const queryClient = new QueryClient(); <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider>
-
Create the fetcher function in
api/posts.js
:import axios from "axios" export const fetchPosts = async ({pageParam=1})=>{ const response = await axios.get(`https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${pageParam}`) return response.data }
-
Create the custom hook in
hooks/useInfinitePosts.js
:import { useInfiniteQuery } from "@tanstack/react-query"; import { fetchPosts } from "../api/posts"; export function useInfinitePosts() { return useInfiniteQuery({ queryKey: ["posts"], queryFn: fetchPosts, getNextPageParam: (_lastPage, allPages) => allPages.length + 1, }); }
-
Build the component in
components/InfiniteScroll/InfiniteScroll.jsx
:import { useRef, useEffect } from "react"; import { useInfinitePosts } from "../../hooks/useInfinitePosts"; export default function InfiniteScroll() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status, error } = useInfinitePosts(); const loaderRef = useRef(); useEffect(() => { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasNextPage) { fetchNextPage(); } }); if (loaderRef.current) observer.observe(loaderRef.current); return () => observer.disconnect(); }, [fetchNextPage, hasNextPage]); if (status === "loading") return <p>Loading...</p>; if (status === "error") return <p>Error: {error.message}</p>; return ( <div className="space-y-4"> {data.pages.map((group, i) => ( <div key={i} className="space-y-2"> {group.map((post) => ( <div key={post.id} className="p-4 border rounded shadow"> <h2 className="font-semibold text-lg">{post.title}</h2> <p className="text-sm text-gray-600">{post.body}</p> </div> ))} </div> ))} <div ref={loaderRef} className="h-10" /> {isFetchingNextPage && <p>Loading more...</p>} </div> ); }
-
Use it inside
App.jsx
:import InfiniteScroll from "./components/InfiniteScroll/InfiniteScroll"; function App() { return ( <div className="max-w-2xl mx-auto p-4"> <h1 className="text-3xl font-bold mb-6">Infinite Scroll</h1> <InfiniteScroll /> </div> ); } export default App;
An auto-loading infinite scroll for blog posts or feeds with a clean Tailwind layout and easy integration into any React app.