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
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.
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> </> ); }
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.
useFetcher
hookWhile 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> ); }
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); };
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.
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> ); }
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();
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 }); };
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>
This allows you to load the page immediately even if the response payload is huge — a big leap over vanilla React.
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.
useEffect
hook; a defacto in plain ReactThat 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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore Fullstory competitors, like LogRocket, to find the best product analytics tool for your digital experience.
Learn how to balance vibrant visuals with accessible, user-centered options like media queries, syntax, and minimized data use.
Learn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.