Creative Ways to Paginate Your Content

To embark on the fascinating journey of paginating your content, you’ll need your Builder account. If you don’t have one yet, you can get started right here!

One impressive method involves a blend of Content API, Custom JS, Element Events, and Element Data Bindings. ( You can check this Content API Exlporer )

Before we dive into the details, if you haven’t already created an application and integrated it with BuilderIO, our Devtools make this process a breeze. Check out the QuickStart here and get ready for an exciting ride! :rocket:

Assuming you’ve already created a page and set up a data model, let’s proceed.

In this example, I’m working with a Movies data model, featuring images and titles for each movie. We’ll paginate this content with 2 items per page.

Our weapon of choice for fetching content is the Content API, but you can also use your own API endpoint for data retrieval.

Now, take a moment to imagine a basic layout for displaying this content, complete with two navigation buttons for pagination:

Once you’ve crafted this page layout with elements, open the Edit Content JS + CSS tab from the data tab and paste the following code into the main function:

Screenshot 2023-08-28 at 1.23.37 PM

Make sure to replace your Builder API key in const MOVIES_CONTENT_API_URL

 const MOVIES_CONTENT_API_URL = "https://cdn.builder.io/api/v3/content/movies?apiKey=<BUILDER_API_KEY>&limit=2&sort.name=1";

    // Initialize state with offset and movieResults
    state.offset = 0;
    state.movieResults = [];

    // Create a function to fetch movies based on the current offset
    async function fetchMovies(offset) {
      try {
        const targetUrl = `${MOVIES_CONTENT_API_URL}&offset=${offset}`;
        const response = await fetch(targetUrl);
        if (response.ok) {
          const data = await response.json();

          // Check if there is no data for the given offset
          if (data.results.length === 0) {
            console.warn('No data found for offset:', offset);
            return null;
          }

          return data;
        } else {
          console.error('Failed to fetch movies');
          return null;
        }
      } catch (error) {
        console.error('Error fetching movies:', error);
        return null;
      }
    }

    // Function to load movies based on the current offset and update state
    function loadMovies() {
      fetchMovies(state.offset)
        .then((data) => {
          if (data) {
            state.movieResults = data;
          }
        })
        .catch((error) => {
          console.error('Error loading movies:', error);
        });
    }

    // Create a function to handle next page
    function nextPage() {
      // Increment the offset by 2
      state.offset += 2;
      loadMovies();
    }

    // Create a function to handle previous page
    function prevPage() {
      // Ensure the offset doesn't go below 0
      if (state.offset >= 2) {
        // Decrement the offset by 2
        state.offset -= 2;
        loadMovies();
      }
    }

    // Bind functions to the state
    state.loadMovies = loadMovies;
    state.nextPage = nextPage;
    state.prevPage = prevPage;

    // Initial load of movies
    loadMovies();

This code fetches movie data from a specific API endpoint. It initializes state variables for the offset and movie results, defines functions to fetch and load movies based on the current offset, and creates functions for handling next and previous pages of movie results. These functions are then bound to the state and used for the initial load of movies.

Query Parameters used: limit, offset, sort. (Querying Cheatsheet)

Now that the data is in place, let’s bind it to a block for repetition. In my case, the results data is in state.movieResults.results. Select the element you want to repeat, expand Element Data Binding, and choose Repeat for each. Your data options should appear as shown below:
Screenshot 2023-08-28 at 2.02.09 PM

Next, bind the image and text elements to the data from your API results. Watch the following Loom video for detailed steps.

Once you’ve completed these actions, publish the page, and voilà! You can now witness your pagination in action. Check it out here.

Exciting news! We’ll soon be adding a count to our Content API.

Another intriguing approach involves moving your logic to the server side and passing data and functions with the BuilderComponent. Here’s an example:

<BuilderComponent
  model="page"
  data={{
    movies: movieList,
  }}
  context={{
    nextPage: () => myService.nextPage(),
    prevPage: () => myService.prevPage(),
  }}
  content={builderJson}
/>

Discover more about custom data and context here.

Alternatively, you can opt to create a custom pagination wrapper component.

:bulb: We’d love to hear your thoughts! Share your ideas and experiences with implementing pagination within Builder. Let’s explore different use cases and address any pain points to enhance the overall experience.

Join the conversation on a similar topic in another post here.

I was attempting to do this with minimal code via a symbol that sits on a page created in the visual editor. Each page has its page number, limit, and a startOffset because I’m also showing some items on the home page.

This has just not been working out all that well for me, unfortunately, having a few issues:

  • The symbol needs data we don’t have yet to render on the server. Since the builder attempts to render the symbol on its server, I get a blank result. This interacts poorly with react’s hydration because it’s able to render client side immediately and that causes hydration to fall back and overwrite everything builder sent. Oops.
  • I actually implemented it much like your suggestion at one point. Unfortunately, it created some sort of mess: once you implement JS in builder’s visual editor, it tries to execute it. It’ll log any errors, but it uses process.env if it detects that it’s rendering server-side. Unfortunately, cloudflare workers do not execute in the node context. you can import process with node compat, but it’s not a global. This causes an uncaught error and I wind up having to delete the page to get things going again. Was thinking about creating an issue around this within the builder.io SDK.
  • When I pass noTraverse: true to the symbol implementation, it does render as expected without the hydration error, but I get a shift from blank (SSR) to having data once the client fully renders.

Tried to pre-seed the data using the content API, but it doesn’t seem to take a data option. Ex:

const page = await builder
    .get("page", {
      url: `/${params["*"] ?? ""}`,
      includeRefs: true,
      options: {
        data: {
          articles,
        },
        noTraverse: false,
      },
    })
    .toPromise();

I’m probably going to go back to the drawing board and move a bit more to code. Go away from the symbol for sure, create loaders for the index and the listing pages.

Update:

Just tried doing this like your alternative passing data to BuilderComponent. It has the same issue that the symbol had: the builder component doesn’t seem to render the bound components until it hits the client. It’s actually worse because noTraverse doesn’t stop it from having the hydration error.

It looks like the best way to do this with SSR is to fetch the data in Content JS, but have to fix the process call in error handling.

Update 2:
Looks like the process error has been resolved as of the latest few builder versions so custom code is back on the table. Still a little sad that the data bindings don’t render well on the server as it is better for that fetching code to live in my codebase than builder’s. Only way I can see around it at this point is making it all some variation of custom component so I’m not asking builder to render anything. Loses the data bindings and visual CMS aspects in that case, so custom code it is!

Thank you for sharing your challenges. It appears there are limitations when rendering symbols with server-side data and executing JavaScript within Builder’s visual editor. While the recent process error resolution is promising, it’s clear that custom code may be the way forward for server-side rendering.

You can explore passing data to components using the following approach:

<BuilderComponent
  model="page"
  data={{
    articles: articlesList
  }}
  content={builderJson}
/>

For more details, please check this link.

In summary, it’s crucial to strike a balance between the convenience of a visual CMS like Builder.io and the control and flexibility offered by custom code. The specific solution will depend on your project’s requirements and your comfort level with code management.

We appreciate your feedback and will discuss it with our team to explore better solutions for future releases. In the meantime, we would like to examine the specific points you’ve highlighted. If you have code snippets or video clips showcasing what you’ve tried, please share them with us. This will help us revisit the issue after implementing improvements.

Your input is valuable, and we’re committed to enhancing your experience with Builder.io. If you have further questions or need assistance, please don’t hesitate to ask.

“Great article! I found the information you provided to be incredibly insightful and well-researched. Your writing style is engaging and made it easy for me to stay focused throughout. Thank you for sharing your knowledge on this topic!”
Free Video and Audio downloader

I was passing data to components just like that. It works, but doesn’t work with referential data and remix because it renders too quickly and breaks hydration.

Ideally, sometime we’d be able to seed the initial fetch with state data as well, so the references and other stuff links through.

I read someone was doing that with the HTML api, but the content SDK doesn’t work that way that I could find.

Thanks for sharing more on that, we’ll definitely take some time out to check this situation and work internally to find a way around this.

I’ve been going around in circles more on how to handle paging and have come to the conclusion that we really need a way to feed data in to the browser side call for symbols. I could add that to the path, but a symbol can appear on any page so it’s not great when you have other pathing going on to force you to add /page/2 or whatever to the URL everywhere.

I’ve been implementing it as a query string ?page=2, but that just doesn’t pass to the server.

e: Implemented paging using the targeting with /page/2 and it does work, but it’s not my favorite. Still have to set noTraverse=true and so the symbol needs another round trip. It’s not loading from server.

Interesting. Even with inheritState: true set, I can see in the return value nested under the symbol this state:

"state": {
  "deviceSize": "large",
  "location": {
     "path": "",
     "query": {}
   }
}

There’s a separate state down below where I expect it to be with the proper values.

Setting noTraverse: true does not populate the symbol, as you would expect, and loads it up when the page is loaded.

Apologies for the delay in responding. I’ve shared your request with our team regarding paginated content and data feeding.

For your specific use case, try setting “Inherit state” to true for the Symbol, which should give you access to the parent state. Here’s a visual representation:

Your feedback is greatly appreciated, and we’re actively working on improvements. Feel free to vote for or suggest ideas on our platform https://ideas.builder.io.

In the meantime, custom code is a suitable solution, and we’re continually enhancing our product for future improvements.