Infinite Scroll with Remix Run
Tags: react, webdev, javascript, tutorial
Published: 2022-02-16 15:01:44

## Introduction
[Remix Run](https://remix.run/docs/en/v1) has landed and has created a lot of buzz in the the javascript framework world. After digging in, it motivated me to try it out by starting a web project that I've put to the side for far too long. One of the main features of this project is viewing a list of an arbitrary amount of photos, fed from a Rails API. This means infinite scroll. A video of the final result is at the bottom.

Traditionally, with client side react, we'd just listen to some breakpoint on scroll or use a virtualization tool package like react-virtual. Then we fire off our own methods, merge the state, and repeat.

Remix Run however, handles its components both on the server (SSR) and in the browser. When I was first trying out infinite scroll, I found that I was fighting against the `loader` and `action` conventions. It felt wrong, and I knew there had to be a better way. Only once I better understood that Remix's url routes are _also it's api routes_, I began to piece it together.

**Reader note**: This infinite scroll is built without any other dependencies (the scroll-breakpoint method). The styling is done with `tailwindcss`, which I think pairs really well with Remix. The Rails API always returns a `json` response. In this case the `fetchPhotos` method returns a response in the format:
```ruby
{ photos: [ { url: "some_url", pid: "12345" }, {...} ] }
```
The `fetchPhotos` abstraction can be replaced with any API or database call for data of your choosing.

We'll solve this problem in three steps:
1. Create a client-side listener for scroll events
2. Establish a determined breakpoint for when the scroll should fire a data fetch
3. Merge the newly fetched data into the current data. 

All while allowing this process to repeat until there is no more data to be fetched. Let's start!

## Leveraging The Scroll
I created a component at `app/routes/photos/_index.tsx`. In Remix, this is accessible in the browser at `/photos`. The Rails API I'm using uses a `page` param to get photos in increments of 10. The initial code to load the page looks like this:

```javascript
import { useLoaderData, LoaderFunction } from "remix";
import { fetchPhotos } from "~/utils/api/restful";
import type { PhotoHash } from "~/utils/api/types";

export const loader: LoaderFunction = async ({ request }) => {
  // Pass a page number to the Rails API
  const resp = await fetchPhotos(1);
  return resp.photos;
};

export default function Photos() {
  const photos = useLoaderData<PhotoHash[]>();

  return (
    <div className="container mx-auto space-y-2 md:space-y-0 md:gap-2 md:grid md:grid-cols-2 py-4">
      {photos.map((photo) => {
        return (
          <div key={photo.pid} className="w-full border-green-200 h-52">
            <img
              className="mx-auto object-center object-cover h-52 rounded hover:shadow-2xl"
              src={photo.url}
              alt={`photo-${photo.pid}`}
            />
          </div>
        );
      })}
    </div>
  );
}
```
But this only gets the first page of photos! `fetchPhotos(1)` is just a call to the Rails server with a first page argument. When the page loads, the `loader` will fetch the photos and render them into the jsx. The problem remains, how do we dynamically fetch more photos during scroll. Keep in mind that Remix also _runs this code on the server_ where there is no `window` or `document`, etc. So we need to rethink our approach!

What we need now is a **client side** listener for scroll events and the browser height. We'll need some React for that:
```javascript
import { useEffect, useState } from "react";
// ...

export default function Photos() {
  const photos = useLoaderData<PhotoHash[]>();
  const [scrollPosition, setScrollPosition] = useState(0);
  const [clientHeight, setClientHeight] = useState(0);

  // Add Listeners to scroll and client resize
  useEffect(() => {
    const scrollListener = () => {
      setClientHeight(window.innerHeight);
      setScrollPosition(window.scrollY);
    };

    // Avoid running during SSR
    if (typeof window !== "undefined") {
      window.addEventListener("scroll", scrollListener);
    }

    // Clean up
    return () => {
      if (typeof window !== "undefined") {
        window.removeEventListener("scroll", scrollListener);
      }
    };
  }, []);

return (
  //...
 )
}
```
Awesome, now if you add a `console.log` inside the `scrollListener` you'll see it fire _very_ frequently whenever you scroll, which is a great start. We also check for the `window`, ensuring the callback only gets attached and removed when the component is rendered on the client. Now we can track where we are during scrolling.

The next step now is to figure out when we want to fire our call to fetch more photos. To do that, we also need to find the height of the parent `div` that contains all the photos. We'll need a `useCallback` listener to attach to that parent:
```javascript
import { useEffect, useState, useCallback } from "react";
//...

export default function Photos() {
  //...
  // We won't care about height until a client-side render
  const [height, setHeight] = useState(null);

  // Set height of the parent container whenever photos are loaded
  const divHeight = useCallback(
    (node) => {
      if (node !== null) {
        setHeight(node.getBoundingClientRect().height);
      }
    },
    [photos.length]
  );

  //...

  return (
    <div ref={divHeight} ...>
      //...
    </div>
  )
}
```
Attaching this method as the `ref` of the parent `div` allows us to set the height into our components state. Furthermore, when we create a dependency on `photos.length` we are ensuring the container's `height` is set properly every time the number of photos change, which would alter the height of that container div.

Now we just need to know when we are firing our API call. We can do a little custom algebra here. **Note: This all varies based upon your layout, the media size (mobile, large desktops, etc). YMMV! At the very least, the parent div should be scrollable from the initial load for this to work properly.**

```javascript
//...
export default Photos() {
  const [shouldFetch, setShouldFetch] = useState(true);
  //...
  
  // Listen on scrolls. Fire on some self-described breakpoint
  useEffect(() => {
    if (!shouldFetch || !height) return;
    if (clientHeight + scrollPosition + 100 < height) return;
    if (fetcher.state == "loading") return;
    console.log("SHOULD BE FETCHING!");

    setShouldFetch(false);
  }, [clientHeight, scrollPosition]);
  //...
}
```
This check `clientHeight + scrollPosition + 100 < height` is the scroll check, and is completely customizable in your own infinite scroll. This fires with the `clientHeight` and `scrollPosition` dependencies so once the criteria is met, we get through the Guard clauses and reach that `console.log` statement. Update: as of a newer version of remix, I've added the next condition `fetcher.state == "loading"`. We need this to ensure we only refetch when the fetcher is not already fetching! Try it yourself, the log should fire once and only once - preventing further calls due to `setShouldFetch(false)`.

## Remix Conventions
So now we want to replace that `console.log` with a real API call and merge them into our photos; allowing this to repeat for as long as there are pages with photo data. But the question with Remix is – where should we call our API fetch from?

Remix's routes _are also its API routes_. This means that if you want to fetch data, well, you should hit your own routes. In this case the route is itself (`/photos`), just with a different `page` parameter (to be sent to our Rails API). Let's look at the `loader` for a second, and add some url query parameter for the page:
```javascript
//...
// Pull page down from the loader's api request
const getPage = (searchParams: URLSearchParams) =>
  Number(searchParams.get("page") || "1");

export const loader: LoaderFunction = async ({ request }) => {
  const page = getPage(new URL(request.url).searchParams);
  const resp = await fetchPhotos(page);
  return resp.photos;
};

export default function Photos() {
  //...
}
```

By parsing the `page` URL parameter, we can now make this `loader` a little bit more dynamic. Hopefully you see where I'm going with this – to fetch more data, we just continuously have to _load_ our data from this route, just with another parameter.

How does Remix do this? There's a nice little hook called `useFetcher` – you can see the documentation on it [here](https://remix.run/docs/en/v1/api/remix#usefetcher). Most of the time, `loader` and `action` are all you need, but `useFetcher` gives us some fantastic flexibility. Specifically, we're gonna leverage our already built `loader`, combined with fetcher's [load method](https://remix.run/docs/en/v1/api/remix#fetcherload). Let's call it when we hit our scrolling criteria:

```javascript
import { useLoaderData, LoaderFunction, useFetcher } from "remix";
//... 

export default function Photos() {
  // Start with two because 1 was pre-loaded
  const [page, setPage] = useState(2); 
  const fetcher = useFetcher();
  //...

  // Listen on scrolls. Fire on some self-described breakpoint
  useEffect(() => {
    if (!shouldFetch || !height) return;
    if (clientHeight + scrollPosition + 100 < height) return;
    if (fetcher.state == "loading") return;
    fetcher.load(`/photos?index&page=${page}`);

    setShouldFetch(false);
  }, [clientHeight, scrollPosition, fetcher.state]);
  //...
}
```

Alright, we are leveraging the fetcher to load the next page (2) when we reach close to the bottom of the parent div. The `fetcher` calls the `loader` from our route, and applies a `page` query parameter. Keep in mind, we add the `index` parameter because this route is an index route; refer to the `useFetcher` docs for greater detail. So now we need to pull the newly fetched photos out and add them to the UI. And what about _continuously_ grabbing more data like "Infinite Scroll" promises? Well, the `fetcher` is stateful, so we'll leverage this in a new, final `useEffect`:

```javascript
//...
export default function Photos() {
  const initialPhotos = useLoaderData<PhotoHash[]>();
  const [photos, setPhotos] = useState(initialPhotos);
  //...

  // Merge photos, increment page, and allow fetching again
  useEffect(() => {
    // Discontinue API calls if the last page has been reached
    if (fetcher.data && fetcher.data.length === 0) {
      setShouldFetch(false);
      return;
    }

    // Photos contain data, merge them and allow the possiblity of another fetch
    if (fetcher.data && fetcher.data.length > 0) {
      setPhotos((prevPhotos: PhotoHash[]) => [...prevPhotos, ...fetcher.data]);
      setPage((page: number) => page + 1);
      setShouldFetch(true);
    }
  }, [fetcher.data]);
  //...
}
```
You can read about `fetcher.data` [here](https://remix.run/docs/en/v1/api/remix#fetcherdata); it's pretty much just the result of `.load()`, which in our case is an array of photos. In this `useEffect`, we depend on `fetcher.data`, and call this whenever that has changed - which directly corresponds to the previous `useEffect` where `fetcher.load(...)` is called.

Let's dive in to this new `useEffect`. When the photos array is 0 we stop fetching all together, meaning we've reached the end of all possible photos (your data source might be different). This prevents the API from being spammed, or making unnecessary calls. 

But when there is photo data in the array, we merge those newly fetched photos with our current photos. Then we increment the page number by one, and finally we set our `shouldFetch` to true, signaling that we want to get the next page when the user has scrolled enough.

Behind the scenes, once the new photos are merged, the container `div` will grow. This causes a re-size of the `height` variable. When a user scrolls, the `useEffect` with `fetcher.load()` will continue to be triggered but the breakpoint has now changed, forcing the Guard clause to return early. _Only when the user scrolls_ and hits that newly calculated breakpoint will the Guard clauses be bypassed, and `fetcher.load()` will be called again with the next page number. This will eventually update `fetcher.data`, causing another photo merge. This process will repeat as the user scrolls, until there is no more data being received from the API.

Below is a little preview of how it works in my browser, along with my console output. You can also view the code for the whole file at this [gist](https://gist.github.com/ptenteromano/e42fe33622a26ba3fdd49b51109203c7).

![Browser infinite scroll, loading on scroll](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k4jc3ydfmoqth4y9w9np.gif)

Thank you all for reading! I hope this helps you deliver a dependency free, Remix-like infinite scroll experience. Let me know if you have any feedback or suggestions in the comments below. Good luck and have fun with Remix!