Next.js vs. Remix - A Developer's Dilemma

The React ecosystem is a bustling landscape, brimming with frameworks promising to revolutionize web development. Today, we’ll be diving into two popular contenders: Next.js and Remix.

Next.js is one of the most popular React frameworks used for server-side rendering. It’s been there for a significant time, and it provides an exceptional developer experience with all the features developers need.

Remix, a newer entrant into the scene, was created by the founders of React Router. It promotes a full-stack development approach and brings several innovative features. With the open-source introduction of Remix in 2022, developers have started to wonder which the better framework for their application is.

Both boast impressive features and passionate communities, but which one should grace our next project?

Let’s break down their strengths and weaknesses to help us pick the champion.

1. Routing

Next.js

Next.js has two different routers: the App Router and the Pages Router. The App Router is a newer router that allows us to use React’s latest features, such as Server Components and Streaming. The Pages Router is the original Next.js router, which allowed us to build server-rendered React applications and continues to be supported for older Next.js applications.

For the app route, Next.js 13 uses directory-based routing where any file under /app called page.tsx gets built as a route. The folders in the app directory can contain layout.tsx for layouts, page.tsx to make that route publicly accessible, loading.tsx to define the loading state and error.tsx for error handling. To create a nested route, we can nest folders inside each other.

Routing

Source: Next.js docs

Folder structure

Source: Next.js docs

Remix

Remix v2, uses a flat-file-based routing system. In our /app/routes folder, we can create new routes by adding new components. Creating a nested route is done using a period delimiter(.) in the file name. For example, if we want to create a /concerts/trending route in our Remix app, we would add a new file called concerts.trending.tsx.

Source: Remix docs

Point of View

Now, if we compare the routing mechanisms of both these frameworks, both have chosen pretty much the same direction for file-system-based routing, and it feels like the right way to go.

Remix seems to be more intuitive, we can tell what route the file/layout represents just by looking at it. But as per Next.js, it also makes sense to put related routing files in one folder which helps to define our loading/error states for each of our route segments.

2. Data Fetching

Next.js

Next.js provides several data fetching methods:

  1. getServerSideProps: Fetches data on the server during each request. This is used for server-side rendering (SSR), where the data is fetched when the page is requested by the client.
  2. getStaticProps: Fetches data at build time, generating static HTML pages with pre-rendered content.
  3. getInitialProps: Runs on both server and client, enabling data fetching for initial rendering and client-side hydration. It is a legacy API.
  4. fetch: Next.js extends the native fetch Web API to allow us to configure the caching and revalidating behavior for each fetch request on the server. fetch with async/await can be used in Server Components, in Route Handlers, and in Server Actions.
async function getUsers() {
  const res = await fetch('https://jsonplaceholder.typicode.com/users')
  if (!res.ok) {
    throw new Error('Failed to fetch data')
  }
  return res.json()
}

export default async function Page() {
  const users = await getUsers()
  return (
    <div>
      <h1>Users</h1>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Remix

In Remix, data is fetched in the loaders. Each route can define a loader function that provides relevant data to the route when rendering. useLoaderData provides the loader’s data to the component. Loaders are only run on the server.

import { useLoaderData } from "@remix-run/react";

export const loader = async () => {
  const users = await getUsers();
  return json({ users });
};

export default function Page() {
  const users = useLoaderData<typeof loader>();
  return (
    <div>
      <h1>Users</h1>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Point of View

Next.js seems to be ideal for applications with a mix of static and dynamic content, where flexibility and customization are prioritized. Remix’s data fetching approach allows for finer control over data loading and dependencies.

3. Data mutations

When dealing with mutations, we often handle them by sending API requests to a backend server and then updating the local state to reflect the changes.

Both frameworks aim to revolutionize mutation handling by integrating it directly into their core features.

Next.js

Before Next.js 13.4, the only way to create and take action on the server was by creating API routes and updating the state.

Next.js 13.4 introduced server actions to handle data mutations to simplify the developer experience and improve the user experience.

// Using API route

export default function Page() {
  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
 
    const formData = new FormData(event.currentTarget);
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: formData,
    });
 
    // Handle response if necessary
    const data = await response.json();
    // ...
  }
 
  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  );
}

//Using server actions

export default function Page() {
  async function create(formData: FormData) {
    'use server';
    const id = await createItem(formData);
  }
 
  return (
    <form action={create}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  );
}

Example taken from Next.js 14 forms and mutations

Remix

Remix automatically keeps the UI in sync with the persistent server state. It happens in three steps:

  1. Route loaders provide data to the UI
  2. Forms post data to route actions that update persistent state
  3. Loader data on the page is automatically revalidated

Remix encourages to keep every part of the application where the user takes an action to be an HTML form. Whenever the user triggers a form submission, it calls the action. Once the action is executed, Remix refetches all the loaders for that route via the browser fetch request and refreshes the UI, ensuring that the UI always stays in sync with the database. This is called the “full-stack data flow” of Remix.

export async function loader({
  request,
}: LoaderFunctionArgs) {
  const user = await getUser(request);
  return json({
    displayName: user.displayName,
    email: user.email,
  });
}

export default function Component() {
  const user = useLoaderData<typeof loader>();
  return (
    <Form method="post" action="/account">
      <h1>Settings for {user.displayName}</h1>

      <input
        name="displayName"
        defaultValue={user.displayName}
      />
      <input name="email" defaultValue={user.email} />

      <button type="submit">Save</button>
    </Form>
  );
}

export async function action({
  request,
}: ActionFunctionArgs) {
  const formData = await request.formData();
  const user = await getUser(request);

  await updateUser(user.id, {
    email: formData.get("email"),
    displayName: formData.get("displayName"),
  });

  return json({ ok: true });
}

The example is taken from Remix route action doc.

Point of View

  1. Next.js server actions are tied with the React ecosystem and APIS from React. Whereas Remix is implemented on what the web platform does and is closely associated with how the web works.
  2. Next.js actions are component-centric. Whereas Remix actions are route-centric so not as composable as components.
  3. In Next.js we need to manually tell to revalidate the path whereas Remix does automatic revalidations

These are tradeoffs of Next.Js and Remix, we can decide which ones we can live with and which we need and decide accordingly.

4. Error handling

Both Next.js and Remix offer mechanisms for handling errors gracefully in our web applications.

Next.js

There is a separate error.js file in each route segment for rendering the error state for that route. The error.js file convention allows us to gracefully handle unexpected runtime errors in nested routes by automatically wrapping a route segment and its nested children in a React Error Boundary. It handles both unexpected errors that might occur on the server or in the browser and expected errors like 404.

Remix

To render the error state for a route segment, we can export ErrorBoundary. It handles both unexpected errors that might occur on the server or in the browser and expected errors like 404.

5. Community Support

Next.js

Next.js is a well-established framework with 118k⭐ GitHub stars (at the time of writing). It has a large community and ecosystem, a significant advantage when looking for solutions to problems, plugins, or integrations.

Remix

Remix has around 26.6k⭐ GitHub stars (at the time of writing) and a growing community.

Point of View

Prefer Remix if the application is not complex and does not need much help from the community. If an application requires a framework with a wider range of features and a large community of users, Next.js is a good choice.

6. Learning Curve

Next.js

Relatively difficult to learn. It provides lots of choices and low-level control might overkill if developers don’t use them correctly.

Remix

Relatively simple. It provides only one way to do things and abstract lots of things out.

7. Deployment

Next.js

Deploying Next.js can be challenging outside of Vercel, which is an excellent platform but may not be ideal if our infrastructure is on AWS. Hosting Next.js in our AWS account offers easier integration with our backend and is often more cost-effective than Vercel. While Next.js doesn’t have native support for self-hosting with serverless, we can run it as a Node application. However, this approach may not provide the same benefits as using Vercel.

Fortunately, there’s a new open-source Next.js serverless adapter - OpenNext. This adapter takes the Next.js build output and converts it into a package that can be deployed to any functions as a service (FaaS) platform, making deployment more flexible.

Kent Dodds expresses his concern about the deployment in his blog.

Remix

Remix was designed for deployment on any platform that supports JavaScript execution. This is largely due to its focus on standards.

8. Pricing

Next.js

Vercel’s pricing seem to be a big problem for a lot of folks. It can be important point to be considered.

However Lee Rob, the VP of Product at Vercel mentioned in his post that they are working on improving the pricing

Remix

As Remix can be deployed on any platform that supports JavaScript execution, we are free to choose the platform as per our choice.

9. Partnership with big brands

Next.js

Next.js is maintained by Vercel. The React team is closely working with the Next.js team to ship new features such as React Server Components.

Remix

Remix has joined forces with Shopify in 2022! Under Shopify’s stewardship, Remix receives long-term backing and support from an established leader in commerce.

10. Companies

Next.js

  1. Netflix Jobs
  2. TikTok
  3. Notion
  4. Loom

The detailed list can be seen here.

Remix

  1. NASA
  2. Docker - Docker Scout is a unified container security solution designed to help developers quickly identify and fix vulnerabilities in all repositories,
  3. Shopify
  4. react-admin - To serve a private npm registry and Enterprise user dashboard.

The detailed list can be seen here.

So, who takes the crown?

And the winner is…

It’s a tie! Both Next.js and Remix excel in different areas.

However, the “best” framework hinges on the project’s unique needs:

For: Large-scale projects, feature-rich frameworks, and quick wins with extensive support - Next.js might be the champion.

For: Performance-critical projects, smooth user experience, solving less complex problems, and a willingness to explore a modern approach - Remix could be the champion.

Remember:

Both frameworks boast active communities and growing resource pools. Hands-on experimentation is key. Build small projects with each to discover the personal fit. The team’s skills and preferences matter. Choose the framework that aligns with the team’s development style.

Happy coding!

Need help on your Ruby on Rails or React project?

Join Our Newsletter