Next.js and Remix — a Brief Comparison

Next.js and Remix — a Brief Comparison

12 Aug 2021

12 Aug 2021

Kacper Łukawski

Kacper Łukawski

Next.js and Remix — a Brief Comparison - cover graphic
Next.js and Remix — a Brief Comparison - cover graphic
Next.js and Remix — a Brief Comparison - cover graphic

See how these two stack up against each other.

Next.js is a well-known React framework. It’s been around for a few years now. It allows you to create feature-rich React applications without compromising performance and SEO. Remix was created to solve the same issues, but with a different approach. I’ve had a chance to work with it recently, here are my thoughts about these two contenders.

Routing

Both frameworks support a file-system-based routing:


Next.js

Next.js


Remix


Next.js uses its own implementation, while Remix utilises React Router underneath. Thanks to this, Remix has an advantage in something that has been a pain point for Next.js for a long time — nested routing with nested layouts.


Nested routes structure


Imagine having a route like [categorySlug]/[subcategorySlug]. Next.js doesn’t provide a built-in solution to share the layout. You would typically reuse the same component for each part of the route.

Parent route:

import { LoaderFunction, useLoaderData } from "remix";

export let loader: LoaderFunction = async ({ params }) => {
  return { slug: params.subcategorySlug };
};

export default function Page() {
  const data = useLoaderData();

  return (
      <div>
        Subcategory slug: {data.slug}
      </div>
  );
}
view raw

Nested route:

import { LoaderFunction, useLoaderData } from "remix";

export let loader: LoaderFunction = async ({ params }) => {
  return { slug: params.subcategorySlug };
};

export default function Page() {
  const data = useLoaderData();

  return (
      <div>
        Subcategory slug: {data.slug}
      </div>
  );
}

In Remix, you can have each route render its part of the layout. And thanks to nesting it will all render on the same page.

Another difference is in the declaration of dynamic routes. In Next.js you use [] to indicate a route with parameter. In Remix, you use $ sign as a prefix in route name.


SSR is the way

Next.js comes with three different page rendering strategies.

Remix comes with a different approach, betting on SSR as the solution. At this point you may think: “Why limit yourself to SSR only? SSG is amazing and ISR is like SSG with superpowers!”.

With Remix, it’s not required to statically generate your pages. You can use other solutions that existed for many years: CDNs and browser caching. Yes, Remix prefers to bet on standards and is built around them. So how does it work?

When you render a page on the server you need to use theCache-Control header. Then you can tell the user’s browser and your CDN provider to cache this page for a specific amount of time.

export const headers = () => {
  return {
    "Cache-Control": "max-age=300, s-maxage=3600"
  };
}

And that’s it.

Yes, it’s that simple. You can cache pages that change less frequently for a longer period. Pages that change frequently, like for example your homepage, might be cached for a short time. And it’s even possible to have the ISR behaviour of regenerating pages in the background on CDN by using the stale-while-revalidate strategy. Although be careful, as not all CDN providers support it.

Taking an SSR-only approach lets Remix run on the edge. That I think is the main advantage it has over Next.js. You can use platforms like Cloudflare Workers to host your web app and get a significant performance boost paying pennies for hosting.


Data fetching

Data fetching in Next.js depends on the chosen rendering strategy:

For each of those strategies, you’ll need some data fetching library (react-query for example) to be able to re-fetch data on the client.

Remix data fetching is also tightly coupled with the page rendering strategy. Each route can implement a loader function. Its responsibility is to fetch your data and return it. Then Remix passes this data to your page. This loader runs on a server so you can directly connect to your database, call external API, read from the filesystem, or do anything you normally do on the server.

export const loader: LoaderFunction = async ({ params }) => {
  const name = await fetchSomeData(params.id);
  return { name };
};

So far it sounds very similar for both frameworks. The main difference will surface when you attempt to access your data.

Next.js passes it as props to your page component.

Remix on the other side has a particular hook called useLoaderData that lets you access your data:

export default function Page() {
  const data = useLoaderData();
  return (
    <h1>
      Hello <b>{data.name}</b>
    </h1>
  );
}

Did you notice? You use this hook inside of your page component. This means it already works for the client-side as well! Yes, Remix comes with a built-in solution for data fetching in both environments — server and client. Isn’t that nice?


Data mutations

Next.js doesn’t have any solutions for data mutations out of the box. You need to either create your own or use existing libraries. That means you need two things:

  1. a way to send your data mutations to your API

  2. a method for user to modify data, probably using some form library for React like Formik

Remix comes with a built-in solution. And yes, it’s also based on standards. HTML forms and HTTP. Let’s dive a bit deeper.

Every route can have an action. It’s just a function, called on the server when your route receives a request with a method other than GET. It’s like a loader, but for data mutations.

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const name = formData.get("name");

  if (typeof name !== "string") {
    return json("Come on, at least try!", { status: 400 });
  }

  if (name !== "John") {
    return json(`Sorry, ${name} is not right.`, { status: 400 });
  }

  return redirect("/dashboard");
};

How do you call your action from the client? By using the Form component from Remix. A very simple form might look like this:

export default function ActionPage() {
  const actionMessage = useActionData<string>();

  return (
    <div>
      <main>
        <h2>Actions!</h2>
        <Form method="post">
          <p>
            <i>What is your name?</i>
          </p>
          <label>
            <div>Name:</div>
            <input name="name" type="text" />
          </label>
          <div>
            <button>Answer!</button>
          </div>
          {actionMessage ? (
            <p>
              <b>{actionMessage}</b>
            </p>
          ) : null}
        </Form>
      </main>
    </div>
  );
}

It’s very similar to a regular HTML form, back from the PHP age. There’s a reason for it. It’s a standard HTML form, but with a progressive enhancement applied. It means you can have a regular form with some fancy UI. This form might even work without any JavaScript. It’s a good, old standard HTML form with the action attribute.

In addition, Remix has two built-in hooks to help you work with data mutations:

  • useActionData — it’s like useLoaderData but for action data

  • useTransition — it gives you form as a serialised FormData (standards!) so you can check its state


Image component

Next.js has a superb component called next/image. Its key benefits are lazy loading out of the box, loading correct image sizes and loaders integration, so you can use services like Cloudinary for image hosting with no additional config.

Unfortunately, Remix doesn’t come with a solution like this. You can create your component or use an existing library. And for lazy loading, you can use the loading attribute for the IMG tag. It lines up nicely with the Remix philosophy of betting on standards, but having it out of the box would be a great addition.


Summary

Those were some of the primary differences between those two frameworks. I’m excited to see Remix impacting the React frameworks world, especially when its major goal is to create better web applications using standards. Also, the possibility to place React app at the edge is something I’m eager to try. Remix is worth trying, particularly for highly dynamic applications. It might surprise you how easy and fun it is to use.

See how these two stack up against each other.

Next.js is a well-known React framework. It’s been around for a few years now. It allows you to create feature-rich React applications without compromising performance and SEO. Remix was created to solve the same issues, but with a different approach. I’ve had a chance to work with it recently, here are my thoughts about these two contenders.

Routing

Both frameworks support a file-system-based routing:


Next.js

Next.js


Remix


Next.js uses its own implementation, while Remix utilises React Router underneath. Thanks to this, Remix has an advantage in something that has been a pain point for Next.js for a long time — nested routing with nested layouts.


Nested routes structure


Imagine having a route like [categorySlug]/[subcategorySlug]. Next.js doesn’t provide a built-in solution to share the layout. You would typically reuse the same component for each part of the route.

Parent route:

import { LoaderFunction, useLoaderData } from "remix";

export let loader: LoaderFunction = async ({ params }) => {
  return { slug: params.subcategorySlug };
};

export default function Page() {
  const data = useLoaderData();

  return (
      <div>
        Subcategory slug: {data.slug}
      </div>
  );
}
view raw

Nested route:

import { LoaderFunction, useLoaderData } from "remix";

export let loader: LoaderFunction = async ({ params }) => {
  return { slug: params.subcategorySlug };
};

export default function Page() {
  const data = useLoaderData();

  return (
      <div>
        Subcategory slug: {data.slug}
      </div>
  );
}

In Remix, you can have each route render its part of the layout. And thanks to nesting it will all render on the same page.

Another difference is in the declaration of dynamic routes. In Next.js you use [] to indicate a route with parameter. In Remix, you use $ sign as a prefix in route name.


SSR is the way

Next.js comes with three different page rendering strategies.

Remix comes with a different approach, betting on SSR as the solution. At this point you may think: “Why limit yourself to SSR only? SSG is amazing and ISR is like SSG with superpowers!”.

With Remix, it’s not required to statically generate your pages. You can use other solutions that existed for many years: CDNs and browser caching. Yes, Remix prefers to bet on standards and is built around them. So how does it work?

When you render a page on the server you need to use theCache-Control header. Then you can tell the user’s browser and your CDN provider to cache this page for a specific amount of time.

export const headers = () => {
  return {
    "Cache-Control": "max-age=300, s-maxage=3600"
  };
}

And that’s it.

Yes, it’s that simple. You can cache pages that change less frequently for a longer period. Pages that change frequently, like for example your homepage, might be cached for a short time. And it’s even possible to have the ISR behaviour of regenerating pages in the background on CDN by using the stale-while-revalidate strategy. Although be careful, as not all CDN providers support it.

Taking an SSR-only approach lets Remix run on the edge. That I think is the main advantage it has over Next.js. You can use platforms like Cloudflare Workers to host your web app and get a significant performance boost paying pennies for hosting.


Data fetching

Data fetching in Next.js depends on the chosen rendering strategy:

For each of those strategies, you’ll need some data fetching library (react-query for example) to be able to re-fetch data on the client.

Remix data fetching is also tightly coupled with the page rendering strategy. Each route can implement a loader function. Its responsibility is to fetch your data and return it. Then Remix passes this data to your page. This loader runs on a server so you can directly connect to your database, call external API, read from the filesystem, or do anything you normally do on the server.

export const loader: LoaderFunction = async ({ params }) => {
  const name = await fetchSomeData(params.id);
  return { name };
};

So far it sounds very similar for both frameworks. The main difference will surface when you attempt to access your data.

Next.js passes it as props to your page component.

Remix on the other side has a particular hook called useLoaderData that lets you access your data:

export default function Page() {
  const data = useLoaderData();
  return (
    <h1>
      Hello <b>{data.name}</b>
    </h1>
  );
}

Did you notice? You use this hook inside of your page component. This means it already works for the client-side as well! Yes, Remix comes with a built-in solution for data fetching in both environments — server and client. Isn’t that nice?


Data mutations

Next.js doesn’t have any solutions for data mutations out of the box. You need to either create your own or use existing libraries. That means you need two things:

  1. a way to send your data mutations to your API

  2. a method for user to modify data, probably using some form library for React like Formik

Remix comes with a built-in solution. And yes, it’s also based on standards. HTML forms and HTTP. Let’s dive a bit deeper.

Every route can have an action. It’s just a function, called on the server when your route receives a request with a method other than GET. It’s like a loader, but for data mutations.

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const name = formData.get("name");

  if (typeof name !== "string") {
    return json("Come on, at least try!", { status: 400 });
  }

  if (name !== "John") {
    return json(`Sorry, ${name} is not right.`, { status: 400 });
  }

  return redirect("/dashboard");
};

How do you call your action from the client? By using the Form component from Remix. A very simple form might look like this:

export default function ActionPage() {
  const actionMessage = useActionData<string>();

  return (
    <div>
      <main>
        <h2>Actions!</h2>
        <Form method="post">
          <p>
            <i>What is your name?</i>
          </p>
          <label>
            <div>Name:</div>
            <input name="name" type="text" />
          </label>
          <div>
            <button>Answer!</button>
          </div>
          {actionMessage ? (
            <p>
              <b>{actionMessage}</b>
            </p>
          ) : null}
        </Form>
      </main>
    </div>
  );
}

It’s very similar to a regular HTML form, back from the PHP age. There’s a reason for it. It’s a standard HTML form, but with a progressive enhancement applied. It means you can have a regular form with some fancy UI. This form might even work without any JavaScript. It’s a good, old standard HTML form with the action attribute.

In addition, Remix has two built-in hooks to help you work with data mutations:

  • useActionData — it’s like useLoaderData but for action data

  • useTransition — it gives you form as a serialised FormData (standards!) so you can check its state


Image component

Next.js has a superb component called next/image. Its key benefits are lazy loading out of the box, loading correct image sizes and loaders integration, so you can use services like Cloudinary for image hosting with no additional config.

Unfortunately, Remix doesn’t come with a solution like this. You can create your component or use an existing library. And for lazy loading, you can use the loading attribute for the IMG tag. It lines up nicely with the Remix philosophy of betting on standards, but having it out of the box would be a great addition.


Summary

Those were some of the primary differences between those two frameworks. I’m excited to see Remix impacting the React frameworks world, especially when its major goal is to create better web applications using standards. Also, the possibility to place React app at the edge is something I’m eager to try. Remix is worth trying, particularly for highly dynamic applications. It might surprise you how easy and fun it is to use.

Start a project

Are you a changemaker? Elevate your vision with a partnership dedicated to amplifying your impact!

💌 Join our newsletter

Receive insightful, innovator-focused content from global product experts — directly in your mail box, always free

Address & company info


Chmielna 73B / 14,
00-801 Warsaw, PL

VAT-EU (NIP): PL7831824606
REGON: 387099056
KRS: 0000861621

💌 Join our newsletter

Receive insightful, innovator-focused content from global product experts — directly in your mail box, always free

Address & company info


Chmielna 73B / 14,
00-801 Warsaw, PL

VAT-EU (NIP): PL7831824606
REGON: 387099056
KRS: 0000861621

💌 Join our newsletter

Receive insightful, innovator-focused content from global product experts — directly in your mail box, always free

Address & company info


Chmielna 73B / 14,
00-801 Warsaw, PL

VAT-EU (NIP): PL7831824606
REGON: 387099056
KRS: 0000861621