Introduction:
In the world of web development, building a todo list application is a rite of passage. In this tutorial, we'll take it a step further by integrating TypeScript for type safety and React Query for efficient data management. By the end, you'll have a fully functional todo list application with robust error handling, real-time updates, and type-safe code.
Step 1: Setting Up Your Project
To get started, let's set up a new React project with TypeScript and React Query. We'll use Vite for fast development and a modern build setup.
npm init vite@latest infinite-scroll-app --template react-ts
after that you have to select react as our option here
and then we will select typescript + swc (Speedy Web Compiler ) you can discover more details about it through this link https://www.dhiwise.com/post/maximize-performance-how-swc-enhances-vite-and-react
After finishing this you have to change directory to the project created and install dependencies
# Change directory
cd infinite-scroll-app
# install dependencies
npm install
# Install React Query
npm install react-query@latest
Step 2: Configuring ReactQuery within our project
In order to make react query works ensure that you've wrapped your application with QueryClientProvider and provided a QueryClient instance.So your main.tsx file will look like this
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
To Get data we will be using Fake APi JSON Placeholder with the Todo endpoint.
Step 3: Fetching data
In this step we will be fetching our data from the API and then we set the limit to 10. The expected behavior for this is to render 10 values on the screen when the page is initially loaded and then continuously add more values when the user reaches the bottom screen.
So for this purpose and to make our project organized we are going to create a folder called services and add file called api.ts inside
Now let's create the function to fetch our data:
export const fetchData = async (page: number) => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos?_pages=${page}&_limit=${10}`
);
const todos = await response.json();
return todos;
};
Now let's go to our App.tsx file and implement the logic of implementing React infinite scrolling
Here, we will use the useInfiniteQuery hook from the React query library. Consider the block of code below:
const { data, error, fetchNextPage, hasNextPage, isFetching, isLoading } = useInfiniteQuery({
queryKey: ["todos"],
queryFn: ({ pageParam }) => fetchData(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
return lastPage.length > 0 ? allPages.length + 1 : undefined;
},
});
The options for useInfiniteQuery are identical to the useQuery hook with the addition of the following:
initialPageParam : Required : The default page param to use when fetching the first page.
getNextPageParam : Required : When new data is received for this query, this function receives both the last page of the infinite list of data and the full array of all pages, as well as pageParam information.
The useInfiniteQuery hook return those properties:
data, which contains the data from the API.
error, which handles any error from fetching the data from the API.
fetchNextpage, which fetches the next set of data to be displayed when the user reaches the bottom.
hasNextpage, which checks to see if there's any more data to be displayed.
isFetching, which shows when the data is currently being fetched.
isLoading, which shows when the first set of data is being loaded from the API.
Step 4: displaying data
In this step we display the list of data that will be renered on our initial page
The data properties for useInfiniteQuery will contain 2 attributes pages and pageParams
- data.pages: TData[] Array containing all pages.
- data.pageParams: unknown[] Array containing all page params. so we are going to format our returned data array before displaying them
const flatData = useMemo(() => data?.pages.flat() ?? [], [data]);
if (isLoading) return <h1>Loading...</h1>;
if (error) return <h1>Error on fetch data...</h1>;
return (
<div>
{flatData?.map((item) => (
<div key={item.id}>
<p>{item.title}</p>
</div>
))}
{isFetching && <div>Fetching more data...</div>}
</div>
);
Step 5: Implementing scrolling behavior
To make the infinte scroll works peerfectly we have first to detect if we have reached the ned of page to trigger a new request and get new data
You might have been wondering how the DOM knows when the user scrolls to the bottom of the page. Well, that's where the IntersectionObserver API comes in! The IntersectionObserver API provides a way to detect an element's position relative to the root element or viewport.
const observer = useRef<IntersectionObserver>();
const lastElementRef = useCallback(
(node: HTMLDivElement) => {
if (isLoading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
console.log(entries);
if (entries[0].isIntersecting && hasNextPage && !isFetching) {
fetchNextPage();
}
});
if (node) observer.current.observe(node);
},
[fetchNextPage, hasNextPage, isFetching, isLoading]
);
This piece of code observes the user's motion on the screen. If the user reaches the bottom of the screen, the IntersectionObserver is alerted and serves the next set of data. Technically, it takes note of the point where the last element in the data intersects and then if hasNextPage is true, it fetches the new set of data to display.
Step 6: Final Result
Full code for this component
import { useInfiniteQuery } from "react-query";
import { fetchData } from "./services/api";
import { useCallback, useMemo, useRef } from "react";
function App() {
const observer = useRef<IntersectionObserver>();
const { data, error, fetchNextPage, hasNextPage, isFetching, isLoading } = useInfiniteQuery({
queryKey: ["todos"],
queryFn: ({ pageParam }) => fetchData(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
return lastPage.length > 0 ? allPages.length + 1 : undefined;
},
});
const flatData = useMemo(() => data?.pages.flat() ?? [], [data]);
const lastElementRef = useCallback(
(node: HTMLDivElement) => {
if (isLoading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
console.log(entries);
if (entries[0].isIntersecting && hasNextPage && !isFetching) {
fetchNextPage();
}
});
if (node) observer.current.observe(node);
},
[fetchNextPage, hasNextPage, isFetching, isLoading]
);
if (isLoading) return <h1>Loading...</h1>;
if (error) return <h1>Error on fetch data...</h1>;
return (
<div>
{flatData?.map((item) => (
<div key={item.id} ref={lastElementRef}>
<p>{item.title}</p>
</div>
))}
{isFetching && <div>Fetching more data...</div>}
</div>
);
}
export default App;
and this will be our result
So that's it guys , I hope you liked of this article and i helped you to learn somthing new :D.
To access the full code for this project and explore further, you can find the repository on my: GitHub
Top comments (0)