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>
</>
);
}
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>
);
}
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);
};
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>
);
}
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();
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 });
};
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.
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:
- Visit https://logrocket.com/signup/ to get an app ID.
- 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');
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>
- (Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- ngrx middleware
- Vuex plugin
Top comments (0)