Implementing React and Firebase Firestore V9 for Paginating to the Initial Page after Navigating Back

I've hit a roadblock here. Despite following the exact steps outlined in various posts and tutorials, I'm still stuck. I know the importance of using a cursor to set the first and last visible document for moving forward or backwards. When moving forward, you start after the last document, but when moving backward, you start before the first.

Everything seems to be working fine when going forward. However, whenever I try to use the previousPage function, it always takes me back to the first page, even though I've correctly set the 'first visible' document. This happens even after moving three pages forward.

There's definitely something missing in my understanding here...

  const PAGE_SIZE = 6;
  const [posts, setPosts] = useState([]);
  const [lastVisible, setLastVisible] = useState(null);
  const [firstVisible, setFirstVisible] = useState(null);
  const [loading, setLoading] = useState(false);

  // Initial read to get first set of posts. 
  useEffect(() => {
    const q = query(
      collectionGroup(db, "bulletins"),
      orderBy("createdAt", "desc"),
      limit(PAGE_SIZE)
    );
    const unsubscribe = onSnapshot(q, (documents) => {
      const tempPosts = [];
      documents.forEach((document) => {
        tempPosts.push({
          id: document.id,
          ...document.data(),
        });
      });
      setPosts(tempPosts);
      setLastVisible(documents.docs[documents.docs.length - 1]);
      setFirstVisible(documents.docs[0]);
    });
    return () => unsubscribe();
  }, []);

  const nextPage = async () => {
    const postsRef = collectionGroup(db, "bulletins");
    const q = query(
      postsRef,
      orderBy("createdAt", "desc"),
      startAfter(lastVisible),
      limit(PAGE_SIZE)
    );
    const documents = await getDocs(q);
    updateState(documents);
  };

  const previousPage = async () => {
    const postsRef = collectionGroup(db, "bulletins");
    const q = query(
      postsRef,
      orderBy("createdAt", "desc"),
      endBefore(firstVisible),
      limit(PAGE_SIZE)
    );
    const documents = await getDocs(q);
    updateState(documents);
  };

  const updateState = (documents) => {
    if (!documents.empty) {
      const tempPosts = [];
      documents.forEach((document) => {
        tempPosts.push({
          id: document.id,
          ...document.data(),
        });
      });
      setPosts(tempPosts);
    }
    if (documents?.docs[0]) {
      setFirstVisible(documents.docs[0]);
    }
    if (documents?.docs[documents.docs.length - 1]) {
      setLastVisible(documents.docs[documents.docs.length - 1]);
    }
  };

Answer №1

It is recommended to utilize the endAt() method instead of endBefore(). Additionally, ensure to provide the order reference, which in this case is the createdAt, to the endAt() method as shown in the code snippet below:

  const PAGE_SIZE = 6;
  const [posts, setPosts] = useState([]);
  const [lastVisible, setLastVisible] = useState(null);
  const [firstVisible, setFirstVisible] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const q = query(
      collectionGroup(db, "bulletins"),
      orderBy("createdAt", "desc"),
      limit(PAGE_SIZE)
    );
    const unsubscribe = onSnapshot(q, (documents) => {
      const tempPosts = [];
      documents.forEach((document) => {
        tempPosts.push({
          id: document.id,
          ...document.data(),
        });
      });
      setPosts(tempPosts);
      setLastVisible(documents.docs[documents.docs.length - 1]);
      setFirstVisible(documents.docs[0]);
    });
    return () => unsubscribe();
  }, []);

  const nextPage = async () => {
    const postsRef = collectionGroup(db, "bulletins");
    const q = query(
      postsRef,
      orderBy("createdAt", "desc"),
      startAfter(lastVisible.data().createdAt), // Include the reference
      limit(PAGE_SIZE)
    );
    const documents = await getDocs(q);
    updateState(documents);
  };

  const previousPage = async () => {
    const postsRef = collection(db, "bulletins");
    const q = query(
      postsRef,
      orderBy("createdAt", "desc"),
      endAt(firstVisible.data().createdAt), // Utilize `endAt()` with the reference
      limitToLast(PAGE_SIZE)
    );
    const documents = await getDocs(q);
    updateState(documents);
  };

  const updateState = (documents) => {
    if (!documents.empty) {
      const tempPosts = [];
      documents.forEach((document) => {
        tempPosts.push({
          id: document.id,
          ...document.data(),
        });
      });
      setPosts(tempPosts);
    }
    if (documents?.docs[0]) {
      setFirstVisible(documents.docs[0]);
    }
    if (documents?.docs[documents.docs.length - 1]) {
      setLastVisible(documents.docs[documents.docs.length - 1]);
    }
  };

For further details, refer to Add a simple cursor to a query.

Answer №2

Introducing a new pagination method tailored for Firebase Database integration with NextJS/React.

Retrieve Data

const PAGE_SIZE = 6;
    const [paginatedPosts, setPaginatedPosts] = useState([]);
    const [lastVisible, setLastVisible] = useState(null);
    const [firstVisible, setFirstVisible] = useState(null);

    useEffect(() => {
        const q = query(
            collectionGroup(database, "posts"),
            orderBy("createdAt", "desc"),
            limit(PAGE_SIZE)
        );
        const unsubscribe = onSnapshot(q, (documents) => {
            const tempPosts = [];
            documents.forEach((document) => {
                tempPosts.push({
                    id: document.id,
                    date: document.data().date,
                    title: document.data().title,
                    title_sub: document.data().title_sub,
                    text: document.data().text,
                    innerImage: document.data().innerImage,
                    goBackLink: "/research"
                });
            });
            setPaginatedPosts(tempPosts);
            setLastVisible(documents.docs[documents.docs.length - 1]);
            setFirstVisible(documents.docs[0]);
        });
        return () => unsubscribe();
    }, []);

    const handleNextPage = async () => {
        console.log('next page')
        const next = query(collectionGroup(database, "posts"),
            orderBy("createdAt", "desc"),
            startAfter(lastVisible),
            limit(PAGE_SIZE));

        const documents = await getDocs(next);

        updateState(documents);
    }

    // createdAt
    const handlePreviousPage = async () => {
        console.log('previous page')
        const next = query(collectionGroup(database, "posts"),
            orderBy("createdAt", "desc"),
            endAt(firstVisible),
            limit(PAGE_SIZE));

        const documents = await getDocs(next);

        updateState(documents);
    }

    const updateState = (documents) => {
        if (!documents.empty) {
            const tempPosts = [];
            documents.forEach((document) => {
                tempPosts.push({
                    id: document.id,
                    date: document.data().date,
                    title: document.data().title,
                    title_sub: document.data().title_sub,
                    text: document.data().text,
                    innerImage: document.data().innerImage,
                    goBackLink: "/research"
                });
            });
            setPaginatedPosts(tempPosts);
        }
        if (documents?.docs[0]) {
            setFirstVisible(documents.docs[0]);
        }
        if (documents?.docs[documents.docs.length - 1]) {
            setLastVisible(documents.docs[documents.docs.length - 1]);
        }
    };

Render Data with Tailwind

<div className="flex items-center justify-center space-y-4 py-5">
    <div className="flex flex-row flex-wrap items-center space-x-2">
        <button onClick={handlePreviousPage} className="inline-flex items-center justify-center w-40 h-10 text-md font-poppins font-light text-white bg-t_brown border-none rounded-full no-underline hover:bg-t_brownlight duration-500 whitespace-nowrap">
            <svg className="w-3.5 h-3.5 mr-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
                <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 5H1m0 0 4 4M1 5l4-4"/>
            </svg>
            Previous
        </button>
        <button onClick={handleNextPage} className="inline-flex items-center justify-center w-40 h-10 text-md font-poppins font-light text-white bg-t_brown border-none rounded-full no-underline hover:bg-t_brownlight duration-500 whitespace-nowrap">
            Next
            <svg className="w-3.5 h-3.5 ml-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
                <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
            </svg>
        </button>
    </div>
</div>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

A guide on customizing column names in MUI Datatables through object keys

I'm currently facing an issue where I need to set the name of a column in MUI Datatables using an object key. Specifically, I want to set one of the column names with the first element of children.childName so that it displays a list of child names, b ...

Chakra UI - The "Open Modal" button is disabled from being clicked repeatedly

Encountering an issue with Chakra UI modal dialog in Next.js. Attempting to utilize the code below in pages/index.tsx for displaying a modal dialog. import type { NextPage } from "next"; import { Modal, ModalOverlay, ModalContent, Moda ...

Link the selector and assign it with its specific value

Greetings, I am a newcomer to React Native and I am currently using Native Base to develop a mobile application. I am in the process of creating a reservation page where I need to implement two Picker components displaying the current day and the next one ...

When incorporating <Suspense> in Next.js, the button's interaction was unexpectedly lost

'use client' import React, { Suspense } from "react"; const AsyncComponent = async () => { const data = await new Promise((r) => { setTimeout(() => { r('Detail'); }, 3000) }) as string; return <div>{d ...

Requires a minimum of two page refreshes to successfully load

Our website is currently hosted on Firebase. However, there seems to be an issue as we have to refresh the website at least twice in order for it to load when visiting www.website.com. Update: We are unsure of what could be causing this problem. W ...

When incorporating a JS React component in TypeScript, an error may occur stating that the JSX element type 'MyComponent' is not a valid constructor function for JSX elements

Currently, I am dealing with a JavaScript legacy project that utilizes the React framework. Within this project, there are React components defined which I wish to reuse in a completely different TypeScript React project. The JavaScript React component is ...

Since updating my dependencies, I've been having trouble running the Android simulator using the command "npx react-native run-android."

Following the update of my dependencies, I am facing an issue where I am unable to build Android on the simulator using the command "npx react-native run-android". Strangely enough, the build is successful when I use Android Studio instead. ...

Ensuring React state is updated accurately

What is the best approach to update the state in React efficiently? I have a dropdown menu with a list of countries and a table that displays the selected country along with its cities. The table should dynamically update based on the user's selection ...

Optimize Material-UI input fields to occupy the entire toolbar

I'm having trouble getting the material-ui app bar example to work as I want. I've created a CodeSandbox example based on the Material-UI website. My Goal: My goal is to make the search field expand fully to the right side of the app bar, regar ...

Issue displaying Windows_NT version 10.0.22621 along with the corresponding logs

NPM issues persisting I keep encountering problems with NPM not working properly, even after attempting npm install multiple times. It keeps generating an npm-debug.log file in another directory. The trouble started when I faced issues with reportwebvital ...

React application that modifies the chosen element when the mouse cursor hovers over

Struggling with changing the selected element on the onMouseEnter event? I have two identical elements and I need to update the style of the one my mouse hovers over. class Form extends React.Component{ constructor(props){ super(props); ...

What is the best way to display multiple sets of data in a MUI Table or DataGrid component?

Could someone provide me with some examples or tips on how to create a table similar to this? view image description here I understand the basics of MUI tables, but I'm unsure how to group the data like shown in the image. Thank you! ...

Implementing material-ui autocomplete with search icon functionality

I am using the @material-ui autocomplete feature for search functionality, and I want to include a search icon next to the autocomplete component. I attempted to achieve this by adding the search icon, but the option fields are no longer displaying proper ...

What is the reason behind the required designation for the props onClose and onOpen in the SwipeableDrawer component of Material-UI?

It seems that the onOpen and onClose properties aim to incorporate elements of the Observer pattern. But why is it necessary for a client using the SwipeableDrawer component who does not wish to observe these events to still provide values for these prope ...

Exploring ways to expand the theme.mixins feature in MUI 5

Currently, I am in the process of updating Material UI from version 4 to 5 and encountering challenges with my existing theming. Since we are using typescript, it is important to include the appropriate types when extending themes. I intend to include th ...

Leveraging the power of useEffect in Next.js to interact with the window object

I encountered an issue when trying to access window.localStorage in my Next.js application. Since Next.js allows for both server-side and client-side rendering, I ran into an error when attempting to set the default value of a state using local storage lik ...

I am facing an issue where using JSON stringify in React Native successfully returns my array, but it is unable to display

Every time I input {alert(JSON.stringify(cart[0]))} in my react-native application, the result displayed is the complete array of objects as shown below: [{ "id": 3, "name": John, . . }] However, when I try {alert(JSON.stringif ...

Using React and Material UI to customize the required label in a text field

In my React application, I am incorporating Material-UI. My goal is to modify the default label "Please fill out this field" of a text-field when setting the required attribute. I attempted to utilize setCustomValidity with inputProps, but unfortunately, ...

The standard installation of NextJS version 13 (13.0.7) encounters issues with the "app" method

Excited to dive into the new project creation process with NextJS 13 and explore the innovative "app" folder concept. I kick things off with these essential commands. npx create-next-app@latest --experimental-app npm run dev Surprisingly, without making a ...

Issue with the Edit feature causing conflicts with the local storage and generating error messages

There seems to be an issue with my edit function that is causing it to override the local storage when I attempt to save and interfering with my delete function as well. I have searched through similar posts for a solution but have not been able to pinpo ...