In our previous lesson, we traced the evolution of the web. We began with static websites, transitioned to server-side rendered dynamic sites, then moved on to Single Page Applications (SPAs), and eventually saw a resurgence in server-side rendering.
In this lesson, we will focus on Remix, take a look at some of its best features, and provide a high-level overview of its internal technical details.
Remix is a full-stack web framework built on top of React, following web standards. We will understand what this exactly means as we progress through this lesson.
Let's look at them one by one.
Server Side Rendering
While learning the history of the web, we saw that one of the eras was of Single Page Application (SPA), where the browser receives an almost empty HTML document, along with JavaScript. This JavaScript is then parsed and starts rendering components' loading state while fetching data for them. This data fetching is recursive. A parent component may have nested child components that fetch their own data, but the parent won't know about them until it fetches its own data.
This often results in a network waterfall request chain, chaining one network request to another. It degrades the user experience as data is fetched one by one for nested components.
The official Remix website homepage has a very good visualization explaining how the network waterfall request chain can lead to an unpleasant user experience.
Several frameworks, such as Gatsby, Hugo, etc., try to solve this using Server Side Generation (SSG). In SSG, all the data for all the pages is fetched during the build time, and all the pages are generated during this phase. The browser receives a fully rendered webpage with all the content. This may solve the majority of the issues that a traditional SPA encounters. The website is fast, as there is no rendering involved on demand, and since the document contains all the content (unlike SPA, where the document is empty), it is great for SEO.
But there are some problems:
Since the data is fetched during the build time, this approach makes sense only if the data on the website rarely changes. And to make even small content changes on the website, the whole site needs to be rebuilt. The build step itself can become very long, as it renders all the pages on the website during build time.
While SSG offers notable advantages over client-side rendered SPAs, it might not be the ideal solution for many modern websites, particularly those with dynamic data. As a result, there has been a renewed interest in Server Side Rendering (SSR), a technique that has its roots in the 1990s.
In Server Side Rendering, the site is generated on demand. This means every time someone visits a page, the data for that page is fetched at that moment and is used to render the document and send it to the browser. This approach is slower compared to SSG (we will see how to make it almost as fast as SSG later), but now our content is generated on demand. And our build step is a lot faster.
SSR combined with Nested Route
However, since the data fetching now happens on the server, the user doesn't see anything until all the data is fetched for the whole page and the page is rendered and sent to the browser. If this was a client-side rendered app, we could have implemented a loading spinner while the data is fetched on the client side. This is more problematic when we need to fetch a large amount of data that are not directly related to each other.
The official Remix website has a great example explaining nested routing. We will use them for explanation in this lesson, in a lot more detail.
Remix solves this with Nested Routes and parallel data fetching. Suppose we are building a sales dashboard. Here, we are fetching two different types of data on the individual invoice page, among many others:
A list of all the invoices.
And data on the individual invoice.
In traditional SSR apps, data for an entire route is fetched at once. For instance, if we're on the invoices/1231
page, both the entire list of invoices and the details for invoice 1231
are loaded. When navigating to invoices/1233
, since the URL changes, the app would typically refetch all data, including the complete invoice list, even though the list hasn't changed. This is redundant and inefficient.
Remix takes a different approach. In a Remix website, we can create /invoices
and invoices/${invoiceID}
as different URLs nested within each other. Consequently, the list of invoices will be downloaded once for the /invoices
route, and this whole component is linked to the route. This means the data will be fetched only once and won't need to be refetched in nested routes when the route changes.
And, the code for route /invoices/${invoiceId}
is only responsible for fetching the individual invoice component.
Remix knows what component to fetch using URL. For example, here, the URL is example.com/sales/invoices/102000
. It knows that it needs to
fetch the root data
sales data,
data for the list of invoices
and individual details on invoice 10200
.
And since it now knows what exactly to fetch for a URL, it can fetch them in parallel. This reduces the time it needs to generate the document, and the browser can receive the fully rendered page much faster.
This can be further optimized by enabling React Streaming. It is a bleeding-edge feature introduced in React 18, which allows sending parts of pages as chunks as they get ready. Since the data is being fetched in parallel, we can send the parts of UI related to those data as they get ready. We will learn more about streaming in detail in a later lesson.
Here, we only saw a high-level overview of how nested routes in Remix make it faster. Later in this course, we have a dedicated lesson on Nested routes, where we go into even more detail and talk about the implementation and some of the best practices when using nested routes.
Error Boundary
One added benefit of nested routing is that it allows adding route level Error Boundary. This means if a part of the nested layout is broken, the error will be shown only in the part of UI related to that part of UI, and the rest of the app continues to work as usual. For example, say for some, our individual invoice route is not getting correct data and is thus broken. We will see the error data only in the individual route component while the rest of the website is functional.
Mutations with actions
A framework isn't truly fullstack unless it has a built-in method for data mutation. Remix utilizes APIs that allow for data mutation using the traditional approach of HTML forms. This seamlessly integrates with the native HTML form element. In addition to this, Remix offers a specialized Form
component. This component is built on top of the HTML form element and brings additional features. It helps prevent race conditions, enables revalidation without a page reload, supports progressive enhancement, and offers many other advantages.
In this example, we are managing a simple to-do list. The loader
function runs on the server and fetches a list of to-dos from the mock database.
The Todos
component then renders this list and provides a form, utilizing the specialized Form
component from Remix, for users to add new items.
Upon form submission, the action
function takes over. It processes the form data to extract the title, creates a new to-do using the fakeCreateTodo
mock database function, and then redirects the user to a page displaying the specific to-do item.
We will learn about all of this in detail in later lessons.
Deploy Anywhere: You own your architecture
Since Remix is built on the Web Fetch API instead of Node.js, this enables it to run anywhere. This can be any Node.js server like Vercel, Netlify, Architect, etc., as well as non-Node.js environments like Cloudflare Worker and Deno Deploy on the edge.
Remix relies on standard Web APIs, but not all server runtimes support these APIs to the same extent. Therefore, the Remix runtime package provides polyfills to bridge any gaps in these features for a given runtime. These polyfills encompass web standard APIs such as Request, Response, and crypto, among others. These are called Adapters in Remix.
In this example, we can see that we can import specific helper functions, such as createCookieSessionStorage
, in this case, from the adapter, based on our server runtime.
If our app is deployed on a Node.js server, then the import will be from @remix-run/node
.
But, if we deploy our app on Cloudflare runtime, such as Cloudflare Pages or Cloudflare workers, we will import the same function from @remix-run/cloudflare
We have a lesson dedicated to choosing the best deployment option for a fullstack Remix app.
Decomposing Remix: Understanding Its Four Core Components
Remix has four core parts: 1. A compiler, 2. A server-side HTTP handler, 3. A server framework, and 4. A browser framework.
At its core, Remix uses a compiler that creates a server HTTP handler, a browser build, and an asset manifest.
While Remix operates on the server, it isn't a server itself; it functions as an HTTP handler given to an actual JavaScript server, which essentially means it processes incoming HTTP requests and responds to them. This modular design allows Remix to be integrated into various existing JavaScript servers. Adapters help integrate Remix with specific servers by converting APIs accordingly.
Remix distinguishes itself by focusing on the UI rather than just the model, as traditional server-side MVC frameworks do. Routes in Remix can handle full or segmental URLs, which become nested layouts in the UI. This allows for a cohesive mix of the UI and controller functions in one file, improving developer efficiency.
This example, that we saw earlier, shows how we can handle everything about a route (or part of a route) in a single file. Here, we are loading data, rendering the UI, and responding to form events on the server in the same file. This is the server framework part of Remix.
On the browser side, once a document has been served, Remix "hydrates" it with JavaScript modules. It enhances user experience by fetching only the necessary data for the next page during navigation, thus avoiding redundant data pulls and maintaining elements like scroll position.
Remix also intelligently prefetches resources based on user navigation patterns, ensuring a swift response even in slow networks. The framework also provides client-side APIs, allowing developers to enhance user experiences without deviating from the fundamental browser model. The essence of Remix lies in its seamless integration of back-end and front-end experiences.
Furthermore, Remix emphasizes Progressive Enhancement, enabling developers to start with basic functionalities and build up without changing the core structure.
Conclusion
In this lesson, we saw some of Remix's superpowers. By combining Server Side Rendering (SSR) with Nested Routes, Remix optimizes data fetching, reduces page load times, and enhances the user experience. It also provides error boundaries, mutation capabilities, and the flexibility to deploy in various environments, making it a versatile choice for modern web development. Remix's four core components, including a compiler, server-side handler, server framework, and browser framework, work seamlessly together to offer a unified and efficient approach to building web applications.
In the coming parts of this course, we will learn about all of them in detail, along with the implementations and examples.
To better understand all the concepts, we will develop a full-stack remix app as we progress through the course. In the next lesson, we will see what the project is, discuss the tech stack, and set up the project.