Remix vs Next.js

It’s actually quite exciting to see Next.js having a new competitor as it has only ever had Gatsby as its main competitor.

In this post, we will compare these frameworks by highlighting what makes each unique and where they shine the most. If you’re new to the world of server-rendered applications, you might probably be wondering, why exactly do we need these frameworks?

Remix vs Next.js: Why Do We Need These Frameworks?

In a traditional React app, the web page is rendered on the client-side where when an initial request is made for that webpage, the browser is sent a shell of an HTML page lacking any pre-rendered content. From there, the browser fetches the JavaScript file responsible for rendering the actual page in the HTML - which is then used to render the appropriate components and content of the requested page - hence the term “Client-Side Rendering (CSR)”.

But user navigation will trigger the client to re-render the HTML file to match the route the user requested. So, the HTML file that is initially sent stays the same, but the components and contents are replaced by JavaScript with those of the requested route - these types of websites are known as Single Page Applications (SPA) as it has client-side routing and only a single HTML file is used to render all of the web pages.

This is the general idea behind Client-Side Rendering (CSR) frameworks such as React, Vue.JS, Svelte, etc. But there are a few major drawbacks associated with client-side rendering:

  • SEO Problems - The content is not reliably indexed by search engines and social media bots as when a bot first hits the webpage, it’s the initial HTML shell that is met.
  • Slow Initial Render - It takes a while to load up the first contentful page when a user first lands, the webpage remains blank until Javascript is loaded and the content is rendered.
  • Issue with caching - Since the actual HTML content is not available in the initial render, the HTML structure of the page cannot be cached.

Hence the birth of dedicated React frameworks for creating websites that are rendered on the server before being sent to the client - Next.js and now Remix.

SSR vs SSG vs ISR

In a bid to solve the drawbacks of client-side rendering came the need to have dedicated frameworks such as Next.js and Remix. These come with the primary purpose to have the requested web pages built and rendered on the server before sending the fully constructed page to the client. As you might have guessed, this automatically solves most of the problems plaguing client-side rendering.

But Next.js and Remix go about server rendering pages differently.

Next.js comes with three different page rendering strategies:

  • Server-Side Rendering (SSR) — where when requested, your pages are always fully rendered on the server on-demand before getting sent to the client. This approach is suitable for websites with a lot of dynamic data and content that change often.
  • Static Site Generation (SSG) — where you can generate all your pages ahead of time during the build time and send only the requested page to the client when prompted. This approach is suitable for static websites that have very little to no dynamic data, else all of the web pages would have to be regenerated whenever there is a change in data.
  • Incremental Static Regeneration (ISR) — which is exactly like SSG, but you can instead set a time interval for your pages to be regenerated at these intervals. This approach is somewhat like a hybrid of SSR and SSG and is suitable for static websites with some dynamic content.

The focus with Next.js is being able to switch between any of these options within the bounds of the framework.

Remix on the other hand takes a different approach by placing all bets on only SSR as the solution. At this point you might be wondering “Why would Remix limit itself to only SSR? SSG is awesome and ISR is like SSG on steroids”.

The entire idea behind Remix going the SSR route is that SSG and ISR have a major flaw, which is when dealing with websites with lots of pages and dynamic data. That would mean having to generate every page at build time - including for hypothetical scenarios e.g. search filter results and such.

This would seem trivial for small scale web apps but imagine having an app that had several pages, like an eCommerce store with several products and a mix of dynamic content. It would generate every single page at build time, and whenever there is a change in data, every page would have to be regenerated - which is not very practical as you can imagine.

Remix decided to take a different approach by making everything SSR because more often than not, websites and applications tend to have dynamic data - but when this is not the case, you may have to resort to SSR + CDN catching or leave the framework entirely. Remix also provides a lot more lower-level APIs than Next.js, like exposing the entire Request object for modifying things like headers before rendering the page - the same is achievable in Next.js using Middleware.

ISR effect on SSR setup

Although Remix doesn’t support SSG, the Remix team suggests using HTTP stale-while-revalidate caching directive (SWR) as an alternative for caching static routes using a CDN. These routes are then served to users on each visit and automatically revalidated for the next visitor. If static pages are of major concern to your Remix app development and you wish to have a truly static route, you can simply cache it at the edge using a CDN.

But, most CDNs use a pull strategy to cache the content, which means the first person in every location will have a slow time to first byte. The nearest edge server needs to pull the content from the origin and then cache it. The experience for those first people will be no different than with CDN disabled.

The alternative to using CDN just as a caching layer is to run Remix app directly at the edge. Currently, it’s only supported using Cloudflare Workers, Fastly and Fly.io. The frontend will be as fast as the backend. If you have a slow API it will block the response and increase TTFB. To optimize it, the Remix team suggests keeping your database at the edge or using Redis. That can add a lot of complexity to your infrastructure.

SSG and ISR are not about caching only. The important feature is that you can roll back to any previous version instantly. It will just work.

Live Reload on Remix vs Next.js

When building web applications in a development environment, it is both time consuming and redundant to have to manually refresh the app whenever a change is made to the source file to see these changes. This is where Hot Module Reloading (HMR) comes into play - which is the ability to patch the code in the DOM (make changes to the source code) without needing to reload the entire application.

The Next.js framework supports React Fast Refresh by default, which allows you to update components and get instantaneous feedback as you save them without having to reload your whole page and losing your app’s client-side state.

Remix on the other hand doesn’t support HMR but supports enabling Live Reload, although it is not enabled by default. You’d have to import a component that will auto-refresh the page as you save files. But thanks to the fast nature of the Remix build server, the reload will often happen before you even notice, although UI state will be lost, there is live reload for server code as well.

Routing on Remix vs Next.js

When it comes to routing, this is where it gets rather interesting - particularly when it comes to Remix. Both Next.js and Remix use a file-based routing system for their respective routes.

In Next.js, the designated routes folder is the “pages” directory found in the root directory and any file or folder created here is treated as such.

Whereas in Remix, the designated routes folder is called “routes” and can be found in the app directory of our application. Whatever file or folder created here is also treated as a route.

Index routes

Both Remix and Next.js use the index root files to automatically route to a parent folder in their respective routes directory - It’s either that or route based on the actual file name.

[@portabletext/react] Unknown block type "markdownContent", specify a component for it in the `components.types` prop
[@portabletext/react] Unknown block type "markdownContent", specify a component for it in the `components.types` prop

Nested routes

Because Remix is built on top of React Router, Remix shines when it comes to nested routing.

It comes with a very powerful route nesting mechanism that can put (nest) or mount other routes in a currently active route to create what would be considered a nested layout of routes. These nested routes behave like actual children routes of the parent route - like they were simply children components that can be mounted and unmounted depending on the active URL path, i.e altering the portion of the current page to reflect the visited nested route.

This nesting is achievable using an Outlet component. The Outlet component is the key to nested routing in Remix as it is used to tell a parent route where to drop off (or mount) a nested child route. Using the Outlet component, you could easily build a hierarchy of deeply nested routes to create a complex nested layout. Through nested routes, Remix can eliminate nearly every loading state as these nested routes are preloaded on the server, creating almost a hybrid of SPA and SSR.

Next.js, on the other hand, comes with its own router and has support for routes nesting but it’s not so easy to achieve a nested routes layout. If you wanted a nested layout, you’d probably need to render the layout on each page manually then add it from the _app page with custom logic - and still, there are some limitations to this workaround when nesting goes beyond two levels.

Dynamic routes

When it comes to wildcards (i.e dynamic routes) - which is the ability to render different content based on URL param using a single dynamic route file. Remix and Next.js have full support for Dynamic routes but have different naming conventions for creating one.

In Next.js it’s created using pair of square brackets with the dynamic param’s name inside.

  • pages/post/[postid].js

Then using the useRouter hook provided by Next.js, the URL query param can be gotten.

Whereas in Remix, it is created by starting the name of the dynamic route with a dollar sign.

  • routes/post/$postid.jsx

And because Remix is based on React Router, the useParam hook can be used to access the URL param.

Data loading on Next.js vs Remix

Fetching data and making API calls in a traditional React app is often done in a hook that is run once a component mounts. As Next.js and Remix are Server-Side frameworks, they each provide their own unique mechanism for handling data fetching, API calls and generally performing asynchronous side effects differently, because the returned data is fetched and used in the component prior to page hydration.

Next.js supports different strategies for loading data depending on the type of web app you’re building i.e SSR, SSG or ISR. This is achievable using several functions provided by Next.js that can be imported and used in our component or route.

  • getServerSideProps - used to load data on the server-side at runtime, the returned data is then provided to the component as props.
  • getStaticProps - used inside a component to fetch data at build time, the returned data is then provided to the component as props. ISR also uses getStaticProps, but with a revalidate prop that sets the interval for re-evaluating the data.
export async function getServerSideProps(context) {
  return {
    props: {} //this data will be passed to the page component as props
  }
}

Or

export async function getStaticProps(context) {
  return {
    props: {} // will be passed to the page component as props
  }
}

Remix on the other hand goes about handling data fetching uniquely with a new concept that involves using:

  • loader function - used to load data in a component, and a supplementary
  • useLoaderData hook - used to get access to the data returned by the loader function.

Every route can have a loader function responsible for fetching data and returning it to your component using the hook. Since this loader runs in parallel on the server, you can directly access your database, call external APIs or do everything else you would normally do on a server.


export const loader = () => {
  // fetch data from database or make API calls
  return {data}
};

export default function App() {
 // get access to data
  let {data} = useLoaderData();

  return (
    <div>
      <p>Use Data in component {data}</h1>
    </div>
  );
}

It is worth noting that the loader is always called loader by convention and when working with dynamic routes, the URL params object is passed to the loader by default and can easily be accessed inside the loader.

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

Side note

Vercel - the company behind Next.js plans to replace getServerSideProps with React Server Components (RSC), which is an experimental feature in React v18 that aims at having everything (including components themselves) rendered on the server before getting sent to the client. This would lead to less bundle size, zero client-side JavaScript needed, and faster page rendering.

They’re also working on enabling Next.js SSR at the edge - which basically means you'll be able to run your entire Next.js server-rendered app at the edge.

Data Mutation on Remix vs Next.js

What happens with performing data mutations i.e allowing users to create and modify data in your application through forms?

Although Next.js has different built-in mechanisms for loading data, it unfortunately has no built-in method for handling forms and data mutation.

Basically, you would have to handle everything yourself:

  • from creating a form,
  • adding state to it,
  • adding an onSubmit event handler,
  • preventing the default behavior of the form,
  • getting the values from the form,
  • sending the form's data to an API or database using fetch,
  • to finally setting loading transition states and catching errors.

Remix on the other hand comes with a built-in solution for data mutation and handles it completely differently by embracing traditional forms using the browser's native HTML form element.

All you need to get a form working in Remix is to either use the traditional HTML form tag or alternatively import a Form component (both work the same) and set up a form with an HTTP request method set to POST or GET, then enter an action URL to send the data to - which by default will be the same as the route for the form. If the method is set to GET, Remix will execute any export loader function defined in the component, but if the method is POST, Remix will execute any export action function defined in the component.

You don't need to hook a state to the form to access the form’s data, instead, Remix passes the form data to these functions as request objects on the server, and you can easily extract the data and perform server-side operations before redirecting the user if need be.

Form validation can be handled on the server and an error key-value object returned back if something is wrong. Moreover, we can use the hook provided by Remix, to know the status of the request and manage the form submission state without manually setting up any loading state. Since the data mutation API is centered around progressively enhanced forms, you can even still retain major functionality for users even without JavaScript.

export const action = async ({ request }) => {
  const form = await request.formData()
  const content = form.get('content')

  return redirect('/')
}

export default function App() {
  return (
    <div>
      <Form method="POST">
        <label htmlFor="content">
          Content: <textarea name="content" />
        </label>

        <input type="submit" value="Add New" />
      </Form>
    </div>
  )
}

Styling on Remix vs Next.js

Next.js comes with support for styled-jsx as a default CSS in JS solution. It also has built-in support for CSS modules and Vanilla CSS (using the _app dir) out of the box.

Adding any other framework or CSS into the JS library is quite simple with a few config settings or plugins.

The primary way to style in Remix is by linking to traditional CSS style sheets placed in the “styles” directory by exporting a Links function in a component. The <Links/> function is used to inject whatever stylesheet which needs to be loaded for a specific route module and the styles are fetched in parallel when loading the route. This stylesheet is then automatically removed when we leave the route to optimize the amount of CSS you're sending per page.

As a result, styling in Remix boils down to using CSS files that can be attached to the website via the <link rel="stylesheet">. There’s support for CSS frameworks and libraries right out of the box, but not those that require bundler or compiler integration. Only those that can generate actual CSS files that can be linked to your remix application. Additionally, Remix supports runtime CSS frameworks like styled components, which are evaluated at runtime but need no bundler integration.

Image Component and Optimization

In Next.js there is a built-in component called next/image that enables automatic image optimization such as lazy loading out of the box, loading images with the correct size, and integrating loaders. It becomes easier to use image hosting services without needing extra configuration.

Unfortunately, at the time I'm writing this article, Remix has no image optimization support. You can, however, import images and get URL back or manually perform simple transformations such as lazy loading using multiple URLs in the srcset attribute.

SEO (Using Link and Meta tags)

One of the primary goals of dedicated server rendering frameworks is to provide better search engine optimization in our web applications. So, it's no surprise that Next.js and Remix both have unique built-in mechanisms for dynamically adding and removing meta info such as page title, description and keywords on the fly when navigating between routes.

In Next.js, this is achievable by importing the built-in next/head as a <Head></Head> component into our pages or routes and then using it to define all the various meta info needed for that page inside this ‘head’ component as direct children.


export default function Page() {
  return (
    <div>
      <Head>
        <title>Page Title</title>
        <meta name="description" content="Page description" />
      </Head>
      <p>
        The meta info in the Head component above gets injected into the
        documents head
      </p>
    </div>
  )
}

It is also worth noting that the contents of the Head component gets cleared when the component gets unmounted and duplicate meta tags can easily be avoided using a unique key prop.

Remix, on the other hand, uses an export meta function to set meta tags. This is done by importing a Meta component that is then placed inside the head of our HTML documents template.

This Meta component looks out for any defined export meta function in the route that is responsible for returning an object containing all the needed meta info for that route as key-value pairs. The ‘Meta’ component then injects the provided meta tags into the page where it is placed and removes them when the route is no longer active.


export const meta = () => {
  return {
    title: 'Page Title',
    description: 'Page description'
  }
}

export default function Page() {
  return (
    <html>
      <head>
        <Meta />
      </head>
      <body>
        <p>
          The Meta component injects the returned properties in the exported
          meta function
        </p>
      </body>
    </html>
  )
}

Error Handling

When it comes to handling errors in Next.js, you have the ability to define some custom pages in the event of certain errors like 404 or 500 errors. But errors beyond routing or status errors like a component failing to render would break the page - this is the standard we have come to expect from frameworks.

Remix introduces a unique concept for handling errors called Error Boundary - introduced in React v16. When we create a route component, we can also create an exported error boundary template that is responsible for catching any error that occurs in this component or route. Think of it as having two components in a single file, one is the actual route's component and the second is the fallback error boundary template that is rendered whenever an error occurs in this route.

A fascinating feature of this error boundary is that it doesn’t need to be set in every component route. When there is an error in a nested component route and this route has no error boundary, the error bubbles up the nested tree until caught by a parent's error boundary. And most importantly, when an error occurs in a nested route, like failing to render, it doesn't take down the entire page in the process - only the context of the part of the app that failed to render would display an error boundary.

Deployment

Deploying Next.js is as easy as running and on any server that supports Node.js. In addition, it has an integration for deploying to Vercel as serverless, and Netlify has developed an adapter for Netlify deployment. A platform adapter could be written to convert platform responses and requests to Next.js responses and requests, but that is not built into Next.js but is documented.

Remix is built on the Web Fetch API instead of Node.js, it can run on both Node.js servers like Vercel, Netlify, Architect, etc. as well as non-Node.js environments like Cloudflare Workers and Deno Deploy.

Right out of the box, Remix offers a platform-agnostic interface and adapters, allowing it to be deployed without configuration to an ever-growing number of service providers. Remix can run natively on Cloudflare Workers and Pages, which unlocks the ability to build applications that run at the edge - closer to the users.

Remix also doesn’t use Webpack anymore under the hood. Instead, it relies on esbuild which makes bundling and deploying extremely fast.

Other Notable Mentions

  • Both Remix and Next.js have out of the box support for Typescript.
  • Remix supports sessions and cookies for Authentication, while Next.js doesn’t - it instead relies on external libraries.
  • Remix retains most of its functionalities with JavasScript turned off, while Next.js doesn’t.
  • Next.js has built-in support for Google AMP, while Remix doesn’t.
  • Next.js has built-in internationalized routing, while Remix doesn’t.
  • Next.js has built-in support for web font optimization, while Remix doesn’t.
  • Next.js has built-in support for script optimization using next/script, while Remix doesn’t.