DEV Community

Cover image for Data fetching with Remix’s loader function
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

Data fetching with Remix’s loader function

Written by Abhinav Anshul✏️

Remix, a full-stack React framework, is designed to perform better with SSR capabilities. Unlike traditional client-side heavy approaches like create-react-app, Remix prioritizes data fetching on the server side before serving the hydrated HTML page. This allows much better loading and SEO performance.

The core idea of Remix's data fetching strategy is to use its loader API. This loader function runs on the server and hence on each page and is responsible for async data fetching before the page or that component renders on the screen.

This ensures data is already available before the page renders, even if you are pulling data from an external API or handling authentications. Loaders make sure HTML is well hydrated with the data that is needed, which also eliminates having too many loading spinners on your page for each fetch call.

However, in contrast, plain or vanilla React uses the [useEffect](https://blog.logrocket.com/useeffect-react-hook-complete-guide/) hook for pulling data from any API. useEffect typically runs on the client side when the page has already been rendered, and you usually put out a loading spinner until you get the data.

This takes a toll on performance and makes initial loading and interaction quite slow. React is perfect for client-heavy interactive apps, as it is unopinionated, but Remix shines for quick page delivery, better initial interactivity, and improved SEO performance.

Basic data fetching with loader API

Let's quickly look at how you can fetch data from an external API using the loader function. You can create this file at the app/routes/posts.jsx. In this component, you can use any external API such as https://jsonplaceholder.typicode.com/posts, which will retrieve all the posts:

import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { Fragment } from 'react';
// loader function that runs on the server
export const loader = async () => {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  if (!response.ok) {
    console.log("Oops! An error has occured", response);
  }
  const posts = await response.json();
  return json(posts, { headers: { "Cache-Control": "public, max-age=60, s-maxage=300" }, });
};
export default function Posts() {
  const posts = useLoaderData(); // data fetched from loader function earlier
  return (
    <>
      <div>
        <div>Blog Posts</div>
        {posts.map(post => (
          <Fragment key={post.id}>
            <div>{post.title}</div>
          </Fragment>
        ))}
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

You can see a loader function that is being used with a simple native fetch call, and the response received is later wrapped with json API imported above. This json is a shortcut for creating an application/json format with optional headers.

Now, coming to the component itself, the formatted posts are now assigned to the useLoaderData hook which will return serialized data that you can map over in the HTML as above.

Post render data fetching with useFetcher hook

While Remix's loader function fetches data before the page renders, there are cases where you need to fetch data after the component has rendered on the page or when the page has already been rendered.

Remix provides a hook for these same situations called useFetcher which allows you to dynamically fetch data post-component rendering without needing to refresh the page. This pattern could be useful for form submissions, updating a particular section of a page like a dynamic search list, dynamic data loading on a button click, etc.

Let's see an example that uses both the initial useLoaderData and the useFetcher hooks to load more posts:

// app/routes/posts.jsx
import { useLoaderData, useFetcher } from "@remix-run/react";
import { useState, useEffect } from "react";
// Loader to fetch initial posts
export const loader = async () => {
  const response = await
  // sample backend url to get list of posts, to fetch initial posts
  fetch("https://jsonplaceholder.typicode.com/posts?_start=0&_limit=5");
  const initialPosts = await response.json();
  return initialPosts;
};
export default function Posts() {
  const initialPosts = useLoaderData();
  const fetcher = useFetcher();
  // logic to get
  const [posts, setPosts] = useState(initialPosts);
  const [start, setStart] = useState(5); // Start at 5 since we already fetched the first 5
  // Append fetched posts to the existing list
  useEffect(() => {
    if (fetcher.data) {
      setPosts((prevPosts) => [...prevPosts, ...fetcher.data]);
    }
  }, [fetcher.data]);
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
      // Button to load more post when clicked
      <button
        onClick={() => {
          fetcher.load(`/api/more-posts?start=${start}&limit=5`);
          setStart((prev) => prev + 5); // Increment start value to load the next set of posts
        }}
        disabled={fetcher.state === "loading"}
      >
        {fetcher.state === "loading" ? "Loading..." : "Load More Posts"}
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The code above fetches the initial five posts when the component is loaded, then whenever "Load More Posts" is clicked, it calls the more-posts API using the fetcher.load API.

To ensure this happens, you have to create a server-side loader called more-post that will, unsurprisingly, fetch more posts from the backend, or in this case, the next set of five posts, IDs 6-10:

// app/routes/api/more-posts.server.jsx
import { json } from "@remix-run/node";
export const loader = async ({ request }) => {
  const url = new URL(request.url);
  const start = url.searchParams.get("start") || 6; // Default start is 6
  const limit = url.searchParams.get("limit") || 5; // Default limit is 5
  const response = await
  // make sure the `start` and `limit` are backend supported
  fetch(`https://jsonplaceholder.typicode.com/posts?start=${start}&limit=${limit}`);
  const morePosts = await response.json();
  // return the data here
  return json(morePosts);
};
Enter fullscreen mode Exit fullscreen mode

Performance considerations

So far, you have seen how Remix's data fetching strategy on the server side helps achieve considerable performance gains, especially when dealing with large-scale apps. In this section, you will see how Remix uses caching and revalidation strategies compared to React.

Faster initial loads with server-side fetching

A key advantage of using Remix's loader hook is that data is already available before the component renders, which leads to faster initial page loads and improves your FCP (first contentful paint).

In contrast, React's client-side rendering employs useEffect Hook for any side-effects including data/API calls to the backend. This slows down your app with a poor FCP score. Here you can see a quick comparison of these two approaches:

// React Client-side data fetching
import { useEffect, useState } from 'react';
function Posts() {
  const [posts, setPosts] = useState([]);

  // this hook runs after the component has rendered, resulting in poor performance
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then(response => response.json())
      .then(data => setPosts(data));
  }, []);

  return (
    <div>
      <ul>
        {posts.map(post => <li key={post.id}>{post.title}</li>)}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The same is achieved in Remix with a server-side strategy using loader:

// create a loader in Remix file
export const loader = async () => {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  return response.json();
};
// using loader hook
import { useLoaderData } from "@remix-run/react";
...
const posts = useLoaderData();
Enter fullscreen mode Exit fullscreen mode

Streaming large datasets to the client

Remix has a really good API called defer that allows the initial HTML to be sent first and the rest streamed progressively to the client, reducing larger payloads at a time on larger data sets. All you have to do is to change your loader function and wrap your returned data with defer:

export const loader = async () => {
  const posts = await fetchLargeData();
  return defer({ posts });
};
Enter fullscreen mode Exit fullscreen mode

Then wrap your component with Remix's Suspense API:

<Suspense fallback={<div>Loading posts...</div>}>
  // streaming data progressively
  <Await resolve={data.posts}>
    {(posts) => (
      <ul>
        {posts.map(post => 
          <li key={post.id}>{post.title}</li>
        )}
      </ul>
    )}
  </Await>
</Suspense>
Enter fullscreen mode Exit fullscreen mode

This allows you to load the page immediately even if the response payload is huge — a big leap over vanilla React.

Trade-offs and complexity: Is it worth it?

Remix comes with a number of advantages, especially in SEO, data fetching simplicity, FCP scores, etc., but there are also some trade-offs and complexities to consider.

  • SSR — Remix is built on top of SSR API designs, it comes with progressive enhancements, better first contentful paint (FCP) and of course better user performance
  • Simplified data fetching — Remix has a way better data fetching strategy via its loader hooks, it has better in-built caching, error-handling that lacks in useEffect hook; a defacto in plain React
  • Increased complexity — Although Remix offers a number of benefits, it could also increase complexity, especially for new developers or projects that don't need to be server-side rendered as it has a steep learning curve especially if your project requirement is small scale applications
  • Infrastructure — With SSR comes proper backend and infrastructure support. So, if you are using traditional cloud functions or server setups, Remix works quite well, but JAMstack and serverless environments would need extra work to set up as they are optimized for serving static hosted files, and a setup like Remix requires more nuanced edge functions and serverless functions like AWS Lambda, Netlify functions, Cloudflare Workers to handle dynamic rendering and routing efficiently

That being, this decision ultimately depends on the type and scale of the project. If SEO is a must for your project and you can put some extra effort to scale your backend, then Remix could be a good choice.

Conclusion

You can see that Remix has very compelling set of APIs that can improve not just data fetching but overall perceived performance of your app. It also helps with a number of lighthouse scores and metrics.

Remix's loader hook for data fetching with an in-built mechanism is a big leap over the traditional useEffect client-side data fetching. If your web app has a primary use case of being a server-rendered site, then Remix would be a better pick.

However, if you have a client-heavy site, you can still go for Remix, as it handles some heavy-lifting on the deployment side. Nonetheless, Remix works quite well on both client and server and is a better pick over vanilla React.


Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.

NPM:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Enter fullscreen mode Exit fullscreen mode
  1. (Optional) Install plugins for deeper integrations with your stack:
  2. Redux middleware
  3. ngrx middleware
  4. Vuex plugin

Get started now

Top comments (0)