Cách triển khai cuộn vô hạn và phân trang với Next.js và TanStack Query

Hầu hết ứng dụng bạn phát triển sẽ đều cần quản lý dữ liệu. Khi các chương trình liên tục mở rộng quy mô, số lượng dữ liệu cần quản lý ngày càng tăng. Khi app thất bại trong việc quản lý dữ liệu hiệu quả, nó sẽ hoạt động kém.

Lập trình Next.js và TanStack Query

Phân trang và cuộn vô hạn là hai kỹ thuật phổ biến mà bạn có thể dùng để tối ưu hóa hiệu suất ứng dụng. Chúng có thể giúp bạn xử lý dữ liệu hiện hiệu quả hơn và nâng cao trải nghiệm người dùng tổng thể.

Phân trang và cuộn vô tận bằng TanStack Query

TanStack Query - một chuyển thể của React Query - là thư viện quản lý trạng thái mạnh mẽ của các ứng dụng JavaScript. Nó cung cấp giải pháp hiệu quả cho việc quản lý trạng thái ứng dụng bên cạnh những chức năng khác, bao gồm các nhiệm vụ liên quan tới dữ liệu như caching.

Lập trình phân trang và cuộn vô hạn

Phân trang liên quan tới việc chia một nhóm dữ liệu lớn thành các trang nhỏ, cho phép người dùng điều hướng nội dung theo nhóm dễ quản lý bằng các nút bấm điều hướng. Ngược lại, cuộn vô hạn cung cấp trải nghiệm duyệt web năng động hơn. Khi người dùng cuộn, dữ liệu mới được tải và hiện tự động, loại bỏ nhu cầu cần điều hướng rõ ràng.

Phân trang và cuộn vô hạn nhắm tới mục tiêu quản lý và hiện dữ liệu lớn hiệu quả. Lựa chọn mộ trong hai phụ thuộc vào yêu cầu dữ liệu của ứng dụng.

Thiết lập dự án Next.js

Để bắt đầu, tạo dự án Next.js. Cài đặt phiên bản mới nhất của Next.js 13 mà sử dụng thư mục App.

npx create-next-app@latest next-project --app

Tiếp theo, cài đặt package TanStack trong dự án bằng npm, trình quản lý gói Node.

npm i @tanstack/react-query

Tích hợp truy vấn TanStack trong app Next.js

Để tích hợp truy vấn TanStack trong dự án Next.js, bạn cần tạo và khởi tạo một phiên bản mới của TanStack Query trong thư mục gốc của ứng dụng - file layout.js. Để làm việc đó, nhập QueryClient QueryClientProvider từ truy vấn TanStack. Sau đó, bao gồm thuộc tính con với QueryClientProvider như sau:

"use client"
import React from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};

export default function RootLayout({ children }) {
  const queryClient = new QueryClient();

  return (
    <html lang="en">
      <body>
        <QueryClientProvider client={queryClient}>
          {children}
        </QueryClientProvider>
      </body>
    </html>
  );
}

export { metadata };

Thiết lập này đảm bảo rằng TanStack Query đã hoàn tất truy cập tới trạng thái của ứng dụng.

Triển khai phân trang bằng hook useQuery

Hook useQuery tìm nạp và quản lý dữ liệu. Bằng cách cung cấp các tham số phân trang, như đánh số trang, bạn dễ dàng truy cập nhóm dữ liệu phụ cụ thể.

Ngoài ra, hook này cung cấp các lựa chọn và cấu hình khác nhau để tùy biến chức năng tìm nạp dữ liệu, bao gồm thiết lập các tùy chọn cache, cũng như xử lý trạng thái tải hiệu quả. Với những tính năng này, bạn có thể tạo trải nghiệm phân trang hiệu quả.

Giờ triển khai phân trang trong app Next.js, tạo file Pagination/page.js trong thư mục src/app. Bên trong file này, tạo các import sau:

"use client"
import React, { useState } from 'react';
import { useQuery} from '@tanstack/react-query';
import './page.styles.css';

Sau đó, xác định thành phần chức năng React. Bên trong thành phần này, bạn cần xác định một hàm tìm nạp dữ liệu từ API bên ngoài. Trong trường hợp này, dùng JSONPlaceholder API để tìm nạp một nhóm bài đăng.

export default function Pagination() {
  const [page, setPage] = useState(1);

  const fetchPosts = async () => {
    try {
      const response = await fetch(`https://jsonplaceholder.typicode.com/posts?
                                  _page=${page}&_limit=10`);

      if (!response.ok) {
        throw new Error('Failed to fetch posts');
      }

      const data = await response.json();
      return data;
    } catch (error) {
      console.error(error);
      throw error;
    }
  };

  // thêm code sau vào đây
}

Giờ xác định hook useQuery, và xác định các tham số sau làm đối tượng:

  const { isLoading, isError, error, data } = useQuery({
    keepPreviousData: true,
    queryKey: ['posts', page],
    queryFn: fetchPosts,
  });

Giá trị keepPreviousData true, điều đó đảm bảo rằng trong khi tìm nạp dữ liệu mới, app sẽ giữ nguyên dữ liệu trước đó. Tham số queryKey là một mảng chứa key cho truy vấn này, vì thế, endpoint và trang hiện tại là nơi bạn muốn tìm nạp dữ liệu cho chúng. Cuối cùng, tham số queryFn, fetchPosts, kích hoạt gọi hàm để tìm nạp dữ liệu.

Như đã nhắc tới ban đầu, hook này cung cấp một số trạng thái mà bạn có thể mở, tương tự như cách bạn sẽ hủy cấu trúc mảng và đối tượng, rồi dùng chúng để cải thiện trải nghiệm người dùng (hiện UI phù hợp) trong khi thực hiện tìm nạp dữ liệu. Những trạng thái này bao gồm isLoading, isError và nhiều hơn thế nữa.

Để làm việc đó, bao gồm code sau để hiện các màn hình thông báo khác nhau dựa trên trạng thái hiện tại của quá trình đang xảy ra:

  if (isLoading) {
    return (<h2>Loading...</h2>);
  }

  if (isError) {
    return (<h2 className="error-message">{error.message}</h2>); 
  }

Cuối cùng, bao gồm code này cho các phần tử JSX mà hiện trên trang trình duyệt. Code này cũng phục vụ cho 2 chức năng khác nhau:

  • Sau khi app này tìm nạp bài đăng từ API, chúng sẽ nằm trong biến data được cung cấp bởi hook useQuery. Biến này giúp quản lý trạng thái của ứng dụng. Sau đó, bạn có thể “map” danh sách bài viết được lưu ở biến này, rồi hiện chúng trên trình duyệt.
  • Để thêm hai nút bấm điều hướng, Previous Next, cho phép người dùng truy vấn và hiện dữ liệu được phân trang bổ sung tương ứng.
  return (
    <div>
      <h2 className="header">Next.js Pagination</h2> 
      {data && (
        <div className="card">
          <ul className="post-list"> 
            {data.map((post) => (
                <li key={post.id} className="post-item">{post.title}</li> 
            ))}
          </ul>
        </div>
      )}
      <div className='btn-container'>
        <button
          onClick={() => setPage(prevState => Math.max(prevState - 1, 0))}
          disabled={page === 1}
          className="prev-button" 
        >Prev Page</button>

        <button
          onClick={() => setPage(prevState => prevState + 1)}
          className="next-button"
        >Next Page</button>
      </div>
    </div>
  );

Cuối cùng, khởi động server lập trình.

npm run dev

Sau đó, tới http://localhost:3000/Pagination trong một trình duyệt.

Phân trang Next.js

Vì bạn đã bao gồm thư mục Pagination trong thư mục app, Next.js xem nó như một lộ trình, cho phép bạn truy cập trang bằng URL đó.

Cuộn vô hạn bằng hook useInfiniteQuery

Cuộn vô hạn cung cấp một trải nghiệm duyệt web liền mạch. YouTube là một ví dụ dễ thấy, nó sẽ tự động tìm nạp video mới và hiện chúng khi bạn cuộn trang.

Hook useInfiniteQuery cho phép bạn triển khai cuộn vô hạn bằng cách tìm nạp dữ liệu từ một server trong các trang và tự động tìm nạp, hiện trang dữ liệu tiếp theo khi người dùng cuộn xuống dưới.

Để triển khai cuộn vô hạn, thêm file InfiniteScroll/page.js trong thư mục src/app. Sau đó, thực hiện các import sau:

"use client"
import React, { useRef, useEffect, useState } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import './page.styles.css';

Tiếp theo, tạo thành phần chức năng React. Bên trong thành phần này, tương tự như cách phân trang, tạo hàm tìm nạp dữ liệu của bài viết.

export default function InfiniteScroll() {
  const listRef = useRef(null);
  const [isLoadingMore, setIsLoadingMore] = useState(false);

  const fetchPosts = async ({ pageParam = 1 }) => {
    try {
      const response = await fetch(`https://jsonplaceholder.typicode.com/posts?
                                  _page=${pageParam}&_limit=5`);

      if (!response.ok) {
        throw new Error('Failed to fetch posts');
      }

      const data = await response.json();
      await new Promise((resolve) => setTimeout(resolve, 2000));
      return data;
    } catch (error) {
      console.error(error);
      throw error;
    }
  };

  // thêm code sau vào đây
}

Khác phân trang, code này đưa ra độ trễ 2 giây khi tìm nạp dữ liệu nhằm cho phép người dùng khám phá dữ liệu hiện tại trong khi cuộn để kích hoạt tìm nạp lại một nhóm dữ liệu mới.

Giờ, xác định hook useInfiniteQuery. Khi gắn kết thành phần ban đầu, hook này sẽ tìm nạp trang dữ liệu đầu tiên từ server. Khi người dùng cuộn xuống dưới, hook này sẽ tự động tìm nạp trang tiếp theo của dữ liệu và hiện nó ở thành phần này.

  const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    getNextPageParam: (lastPage, allPages) => {
      if (lastPage.length < 5) {
        return undefined;
      }
      return allPages.length + 1;
    },
  });

  const posts = data ? data.pages.flatMap((page) => page) : [];

Biến posts kết hợp tất cả bài viết từ các trang khác nhau thành một mảng, cho một phiên bản đơn giản của biến data. Điều này cho phép bạn dễ dàng ánh xạ và hiện từng bài viết.

Để theo dõi cuộn người dùng và tải nhiều dữ liệu hơn khi người dùng tới gần phía dưới cùng của danh sách, bạn có thể xác định một hàm sử dụng Intersection Observer API để phát hiện khi nào các thành phần giao với khung hình.

  const handleIntersection = (entries) => {
    if (entries[0].isIntersecting && hasNextPage && !isFetching && !isLoadingMore) {
      setIsLoadingMore(true);
      fetchNextPage();
    }
  };

  useEffect(() => {
    const observer = new IntersectionObserver(handleIntersection, { threshold: 0.1 });

    if (listRef.current) {
      observer.observe(listRef.current);
    }

    return () => {
      if (listRef.current) {
        observer.unobserve(listRef.current);
      }
    };
  }, [listRef, handleIntersection]);

  useEffect(() => {
    if (!isFetching) {
      setIsLoadingMore(false);
    }
  }, [isFetching]);

Cuối cùng, bao gồm các thành phần JSX cho bài viết hiện trong trình duyệt.

  return (
    <div>
      <h2 className="header">Infinite Scroll</h2>
      <ul ref={listRef} className="post-list">
        {posts.map((post) => (
          <li key={post.id} className="post-item">
            {post.title}
          </li>
        ))}
      </ul>
      <div className="loading-indicator">
        {isFetching ? 'Fetching...' : isLoadingMore ? 'Loading more...' : null}
      </div>
    </div>
  );

Sau khi bạn đã thực hiện tất cả thay đổi cần thiết, tới http://localhost:3000/InfiniteScroll để quan sát hoạt động của chúng.

Thế là xong! Hi vọng bài viết hữu ích với các bạn.

Thứ Hai, 06/11/2023 17:14
51 👨 459
0 Bình luận
Sắp xếp theo