Table of Contents
Nowadays we have many different framework options when we want to create a new web project based on React. As a developer, you can find yourself struggling to know which one should you choose or which one would best suit your needs.
One of the most used frameworks you may know is Next.js, commonly used by companies like Netflix, Twitch, or Uber. It is considered one of the fastest-growing React frameworks.
It has been difficult for others to compete with Next.js as it covers three different page rendering strategies, but since November 2021 it seems we have a new, fresh and powerful framework called Remix.
Sorry Gatsby I didn’t forget you and I love the way you work when generating static sites
Why do you need Next.js or Remix instead of plain React
Maybe you don’t, it depends. The point is to understand how React applications work, which problems you could find and how frameworks such as Next.js or Remix work to solve them.
The point about React is that you can make Single Page Apps (SPA), where only a single HTML file is used to render all web pages and the routing stays on the client-side. But what does this mean exactly?
- When the initial request is made, the browser receives an HTML box page that contains all the applications at once.
- No pre-rendered content
- When user navigation triggers, JavaScript replaces only those components and content related to the requested route, but the HTML file stays the same.
In short, the browser is the one managing which JavaScript file should be loaded to render the current page. In other words Client-Side Rendering (CSR)
CSR can be very fancy and helpful to create applications that don’t need to care about SEO, caching or slow initial render.
- SSlow Initial Render – When a user first lands it could take a long time to load up the first contentful page. This means to let them get a blank page until JavaScript loads and everything is rendered.
- SEO – As it will have only a single HTML page the content it’s quite difficult to get the content indexed by search engines and social media bots.
- Caching problems – the HTML structure cannot be cached as it is not available on the first render.
At this point you may think that SPAs are the devil, but think about all those internal applications that so many companies use, or the application products they sell.
Next.js or Remix appears when you want to take advantage of how SPAs work without losing the three points I mentioned before. The strong point is that a website can be rendered on the server side before sending it to the client.
SSR vs SSG vs ISR
I have already explained what does mean and how CSR works, now it’s my turn to talk about other fancy page rendering strategies.
Next.js is a powerful option as it comes with 3 different options out of the box while Remix relies fully on SSR (at this moment). But how do these strategies work exactly?
Server-Side Rendering (SSR)
When a page is requested, it is fully rendered on the server before it is sent to the client. Being a great option for those websites with a lot of dynamic data and content.
Static Site Generation (SSG)
This strategy generates all pages by route during build time. Only the requested page is sent to the client when prompted. In other words, this constructs a typical static website.
It can be a suitable option for those static websites with little to no dynamic data and not so many pages. Each change in the data will trigger a regeneration of all the pages.
Incremental Static Regeneration (ISR)
This concept has been offered by Next.js and is kind of a hybrid of SSR and SSG.
It is like SSG but you can set an interval time to let know when your pages should be regenerated. Suitable for those static sites with dynamic content.
The strong point about Next.js is that it lets you switch between those 3 options on each page, so you can have each of them following different strategies.
So maybe you ask yourself “why should I replace Next.js with Remix if it only works with SSR”. And the answer is pretty simple, nowadays applications and websites tend to have dynamic data and there are few examples that would suit an SSG or ISR scenario.
Taking this into account, Remix provides a lower-level API. For example, it exposes the Request object so you can easily modify headers before rendering the page, while in Next.js you would need middleware to achieve it.
Routing
Both frameworks use a file-based routing system to render the pages.
- Each page remains in a different file.
- Each page is associated with a route based on its file name.
- Each page, in the end, renders a React Component.
Routing in Remix
You will add each route in /app/routes
and every file or folder created inside it will be treated as a route.
Also, you will find a file in /app/root.jsx
which is the “root route” and is the layout for the entire application. It contains the <html>, <head> and <body> tags, as well as other Remix related stuff.
Routing in Next.js
You will add each route in /pages
and it works exactly in the same way as Remix.
In this case, you may, or not, have a pages/_app.js
and a pages/_document.js
. Where App is used to initialize pages and Document contains the, and tags.
Next.js uses them by default with no needed extra configuration. But, in case you need to customize them you can create those two files, overriding this way the default ones.
Index routes
Both of them use the same system to render a container route.
Example with Next.js:
pages/index.js
=>/
(root page)pages/posts/index.js
ORpages/posts.js
=>/posts
Example with Remix:
routes/index.js
=>/
(root page)routes/posts/index.js
ORroutes/posts.js
=>/posts
Nested routes
Remix is built on top of React Router and by the same team so you can imagine who is the winner when it comes to nesting routes.
The idea behind it is to make an active route to mount a layout containing multiple nested routes, so each route manages its own content.
If I had to explain it in React words I would tell you to imagine every nested route were components and depending on the URL path they can be mounted and unmounted.
To achieve this Remix uses the <Outlet />
React-router component. It is used on parent routes to tell them to render child route elements whenever they are triggered.
Remember all these nested routes are preloaded on the server so almost every loading state can be removed. Doesn’t it sound like a mix between SSR and SPA?
Next.js on the other side does support nested routes but they work as a separate page. If you wanted to achieve something similar to what Remix does you probably should customize your _app.js
.
Example with both:
routes/posts/news/index.js
=>/posts/news
Dynamic routes
They work by rendering different content based on a URL param that is dynamic, but it uses a single route file.
In this case, both frameworks do support this functionality but use different naming conventions.
Example with Next.js
pages/posts/[postId].js
=>/posts/some-post-slug
pages/products/[category]/[productId].js
=>/products/keyboards/some-keyboard-slug
OR/products/headphones/some-headphone-slug
It has its own hook, useRouter
that will provide you the current value for those dynamic params (postId, category, productId).
Example with Remix
/app/routes/posts/$postId.js
=>/posts/some-post-slug
/app/routes/products/$category/$productId.js
=>/products/keyboards/some-keyboard-slug
OR/products/headphones/some-headphone-slug
You could use useParam
React Router hook to access those dynamic parameters.
What about routes that need to load files?
Imagine you need your website to contain the typical robots.txt
and sitemap.xml
files.
Both in Next.js and Remix you could add them to /public directory. But Remix lets you achieve it through the routing system, simply by creating a /app/routes/[sitemap.xml].js
or /app/routes/[robots.txt].js
Data Loading
Both frameworks are Server-Sided so each one has different systems to fetch data and hand API calls.
In the case of Next.js you can choose between two options, depending on what type of page you want to build.
- getStaticProps (SSG, ISR if revalidate interval is set) – it fetches data at build time and provides its data as the page’s component props.
export async function getStaticProps( {params} ) {
const productList = await getProductList()
return {
props: {
productList,
slug: params.slug
}
}
}
- getServerSideProps (SSR) – it fetched data on the server-side at runtime and provides the returned data as the page’s component props.
export async function getStaticProps( {params} ) {
const productList = await getProductList()
return {
props: {
productList,
slug: params.slug
}
}
}
/**
* Here you would have your own getStaticProps/getServerSideProps functions
* depending on what system you choose.
**/
const PageName = (props)=> {
const { productList } = props
// Here you could render the requested data
return (<div>
<pre> {JSON.stringify(productList, null, 4)} </pre>
</div>)
}
Remix uses a different way to load data
- It provides you with a
loader
function to use on each route file which will work on the server-side at runtime. - On the page component itself, you will then be able to use
useLoaderData
hook to gather this data.
/**
* Here you would have your own getStaticProps/getServerSideProps functions
* depending on what system you choose.
**/
export const async loader = ( {params} ) => {
const productList = await getProductList()
return {
productList,
slug: params.slug
}
};
export default function PageName() {
const { productList } = useLoaderData();
// Here you could render the requested data
return (<div>
<pre> {JSON.stringify(productList, null, 4)} </pre>
</div>)
}
Data Mutations
Next.js doesn’t have a built-in way to perform mutations and you have to make it manually.
Remix on the other side has created a Form Component and uses it as you would do with a browser’s native HTML Form element. If you don’t enter any action URL it will use the same as the route for the form.
If the method is a GET, the loader
the exported function will be triggered, while if the method is a POST, the action
exported function defined in the component will be triggered.
Also, you can use the provided useTransition hook to manage the behavior of the application depending on the request status without having to manage it manually as usual.
export const loader = async ({ params }) => {
// Each time this page receive a GET Request, this loader will be executed
const userData = await getSomeUserData(params.slug)
return {
userData // it contains the User Name
}
}
export const action = async ({ request }) => {
// Each time this page receive a POST Request, this loader will be executed
const form = await request.formData()
const content = form.get('content')
return redirect('/')
}
export default function App() {
const { name } = useLoaderData();
return (
<div>
<div>{`Hello there ${name}`}</div>
<Form method="POST">
<label htmlFor="content">
Content: <textarea name="content" />
</label>
<input type="submit" value="Add New" />
</Form>
</div>
)
}
Styling
Next.js comes with built-in CSS support out of the box so you can import it directly on your /pages/_app.js any style.css file you need.
You can also add other CSS frameworks, with some config or plugins is pretty easy to achieve.
Remix prefers to focus on a more web-standards-friendly solution by providing a component and an links
exported function.
import styles from "./styles/app.css";
export function links() {
return [
{ rel: "stylesheet", href: styles },
{ rel: "stylesheet", href: 'https://.....' },
];
}
export default function App() {
return (
<html lang="en">
<head>
<Links />
</head>
<body>
<Outlet />
</body>
</html>
);
}
You could think this is a little too much to simply load a stylesheet but, what if I tell you you can use it on each page, separately, letting you load specific styles/fonts only when needed?
Also here you can use other CSS frameworks with little configuration, such as Tailwindcss.
Image Optimization
Here Next.js wins as it has it’s own built-in component next/image
that enables image optimization, lazy loading, size control and integrates loaders. Unfortunately at this time Remix doesn’t have any image optimization support.
SEO
At the beginning of this article, I told you that one of the points behind these frameworks is to solve SPAs SEO issues.
Both of them have their own built-in mechanisms to manage dynamically what metadata should be used on each page, such as keywords, titles, descriptions, etc.
In Next.js you can use their built-in component next/head
by importing it on your pages/routes.
import Head from 'next/head'
export default function PostPage() {
return (
<div>
<Head>
<title>Post Page Title</title>
<meta name="description" content="Post Page Description" />
</Head>
<p>
All the metadata inside Head component will be injected into the
documents head
</p>
</div>
)
}
On the other hand, Remix provides us a meta
exported function that can receive the page’s requested data as well as URL parameters.
export const meta = ({ data, params }) => {
charset: "utf-8",
viewport: "width=device-width,initial-scale=1",
title: `Post | ${data.title}`,
description: data.description,
};
export default function PostPage() {
return (
<div>
<p>
All the metadata returned on meta function will be injected into the
documents head
</p>
</div>
)
}
Error Handling
Next.js gives you the possibility to define custom pages when certain errors are caught, like 400 or 500 errors. But any other error beyond routing or status errors would cause the page to break.
Remix leaves it to your imagination so you can cover and customize every error you want. They have added Error Boundary, a unique concept for handling errors – introduced in React v16.
You can cover those errors easily by using CatchBoundary
and ErrorBoundary
exported functions on your root.tsx
or even on each page. Let’s add some visual examples of this.
// Here you will catch any controlled error and will tell the framework what information should be shown
const CatchBoundary = () => {
let caught = useCatch();
switch (caught.status) {
case 401:
// Document is a custom React Component which contains <html>, <head>,<body>, etc
return (
<Document title={`${caught.status} ${caught.statusText}`}>
<h1>
{caught.status} {caught.statusText}
</h1>
</Document>
);
case 404:
return <PageNotFoundComponent />; // Yes, you can also use a React Component
default:
throw new Error(
`Unexpected caught response with status: ${caught.status}`
);
}
};
// Here you will have a more generic advise for when an uncontrolled error has been triggered
const ErrorBoundary = ( { error } ) => {
return (
<Document title="Uh-oh!">
<h1>App Error</h1>
<pre>{error.message}</pre>
<p>
Replace this UI with what you want users to see when your app throws
uncaught errors.
</p>
</Document>
);
};
export const loader: LoaderFunction = async ({ params }) => {
const post = await getPost(params.slug);
if (!post) {
throw new Response("Not Found", {
status: 404,
});
}
return json({ post });
};
export default function PostPage() {
const { post } = useLoaderData();
return (
<div>
<pre>
{JSON.stringify(post, null, 4)}
</pre>
</div>
)
}
Deployment
Next.js is easy to deploy on any server that supports Node.js. It also has an integration for deploying as serverless to Vercel, and Netlify has its own adapter so you can easily get your application deployed.
Remix is built on the Web Fetch API instead of Node.js so it can run on Vercel, Netlify, Architect (Node.js servers) as well as non-Node.js environments like Cloudflare.
It also provides you out-of-the-box with adapters when you start your project so you can go straight to the point if you know where you will be deploying your project.
And, one of the most important things about Remix is that it doesn’t use Webpack anymore. Instead, it uses Esbuild which is extremely fast in bundling and deploying.
Other mentions
There are other points to have into account whenever you want to choose Next.js or Remix but I wanted to collect those that, in my opinion, have more impact when developing a project.
But if you want to know a bit more, here is a little recap of things I haven’t talked about in this article.
- Live Reload. Next.js uses Hot Module Reloading (HMR) and is enabled by default, while on Remix you have to add a component on your root.tsx to force your application to reload whenever the code is changed.
- Remix supports Cookies, Sessions, and Authentication while Next.js doesn’t – you must use external libraries.
- Next.js has built-in support for font and script optimization, while Remix doesn’t.
- Both let you use Typescript out of the box.
- Remix work through most of its functionalities without executing JavaScript while Next.js doesn’t.
- Remix can contain independent routes inside nested ones
- Both can work quickly with Tailwindcss and have their own guide to achieve it.
- Next.js has built-in support for internationalized routing.
- Next.js has out-of-the-box support for AMP while Remix doesn’t.