How Remix and Next.js are Redefining Full-Stack Web Development

Bridging the gap: How Remix and Next.js are redefining full-stack web development

Symphony Logo
Symphony
November 12th, 2024

React has come a long way since its inception, and recent developments are opening up new possibilities for web development. Modern frameworks are now exploring ways to seamlessly integrate frontend and backend capabilities, offering developers new tools to create more cohesive web applications.

These frameworks are introducing innovative approaches that extend React's component model to the server while also incorporating robust backend functionalities. As a result, developers are discovering more streamlined and powerful ways to build web applications.

Key areas of change include:

  1. Writing React: The way we compose React applications is evolving. Server components, streaming SSR, and other new patterns are changing how we structure our code and think about data flow.
  2. Server-to-Server Interactions: These frameworks are reimagining how the frontend communicates with backend services, often simplifying and optimizing these interactions.
  3. Unified Backend and Frontend: Perhaps most importantly, these frameworks are positioning themselves as comprehensive solutions that can serve as both the backend and frontend of web applications.

At the core of any web application are two fundamental processes which we will explore in this post, and those are:

  1. Data querying (how we get the data to the user’s browser and make sure it’s fresh)
  2. Data mutation (How we change it over time)

Traditionally, both data querying and data mutation has been implemented through communication with the JSON APIs written in server side languages like PHP, Go, Python, Ruby and many others, with the browser making calls to the server.

This post aims to explore an alternative approach: the use of frameworks that integrate Node.js and React capabilities into one. These frameworks offer a different paradigm for handling data flow and mutations. Drawing from my experience primarily as a frontend developer, with some exposure to backend development, I'll discuss the potential advantages and drawbacks of this approach.

Data Querying

Data querying is a fundamental aspect of web application development, determining how information is retrieved and presented to users. In modern React-based frameworks, the approach to data querying has evolved significantly, moving beyond traditional client-side fetching to more efficient server-side methods. This section explores how two popular frameworks, Remix and Next.js, handle data querying, highlighting their unique approaches and the benefits they offer to developers.

Remix Loaders

Remix represents an innovative approach to web development, seamlessly blending Node.js capabilities with React's component model. At its core, Remix introduces two powerful concepts: loaders and actions. These features exemplify how Remix bridges the gap between backend and frontend, offering developers a unified way to handle data querying and mutations.

Loaders in Remix are server-side functions that fetch and prepare data for your React components and here's why they're significant:

  1. Improved Performance: By moving data fetching to the server, Remix reduces client-side network requests and payload sizes.
  2. Simplified Data Flow: Loaders provide a clear, predictable pattern for getting data into your components.
  3. Access to Backend Resources: Loaders can directly interact with databases, file systems, and other server-side resources.
  4. Data Shaping: You can shape your data on the server before sending it to the client, reducing the need for client-side data manipulation.

Here's an example of a loader function that checks for user’s authentication and fetches a list of projects:

loader function for user’s authentication

In our example loader, we're leveraging several key Remix features. The request object, passed as part of the loader's arguments, encapsulates all the information about the incoming HTTP request. We use this to authenticate the user via the authenticator.isAuthenticated() method. If authentication fails, we redirect the user to the login page using a server side redirect built into Remix.

Once authenticated, we perform a series of checks and data fetching operations. We verify if the user's account is activated, redirecting them to an activation page if necessary. Then, we query our database for the user's projects using Drizzle ORM's query builder. This showcases how Remix allows us to seamlessly integrate with various database libraries and ORMs.

After fetching the projects, we apply some business logic, filtering out archived projects and checking if the user has any active projects. If not, we redirect them to create a new project. This demonstrates how loaders can not only fetch data but also control application flow based on that data.

Finally, we prepare and return the data that will be sent to the client. By defining a LoaderData type, we ensure type safety between our server-side data fetching and client-side rendering. This typed approach helps prevent runtime errors and improves developer experience.

Remix's loader system exemplifies the framework's commitment to progressive enhancement and separation of concerns. By associating loaders with specific routes, we can define precise data requirements for each page or section of our app. This route-based approach to data loading ensures that only necessary data is fetched, contributing to faster page loads and a more efficient application overall.

In essence, Remix loaders provide a robust foundation for building secure, performant, and maintainable web applications. They enable us to craft experiences that work well across a wide range of devices and network conditions, all while keeping our codebase clean and our data handling secure.

This is the example of how we can consume the data that is returned from our loader, using a hook that comes with Remix, useLoaderData:

Remix hook in loader

This approach works well, but as mentioned earlier, loaders are tightly tied to routes. This means we must load all necessary data in one function, and components can only consume data—they can't make requests to fetch it. React Server Components (RSC), a new feature of React, aims to change this approach. Currently, Next.js is the only popular framework implementing RSC, so we'll examine its approach to data querying.

Next.js and React Server Components

React Server Components allow developers to write React components that are executed and rendered entirely on the server. This is different from traditional server-side rendering, where the entire app is rendered on the server and then hydrated on the client.

With Server Components:

  1. The component's logic runs on the server, not in the user's browser.
  2. Only the rendered HTML is sent to the client, not the component's JavaScript code.
  3. They can seamlessly integrate with client-side components in the same application.

Key difference between the Remix model and the newer React Server Components (RSC) model is that RSC can be placed anywhere in the app and be rendered on the server. For example, a deeply nested component in your React tree can be a Server Component, allowing for more granular control over server-side rendering.

In Remix, server-side operations are typically confined to route-level loaders and actions. RSC, on the other hand, allows you to:

  1. Sprinkle server-rendered components throughout your app, even within client components.
  2. Fetch data at the component level rather than just at the route level.
  3. Keep sensitive operations or large dependencies server-side, even for non-route components.

This flexibility can lead to more optimized applications, as developers can make fine-grained decisions about what runs on the server versus the client, potentially improving both performance and security.

Let's look at our previous example implemented as a React Server Component in Next.js:

React Server Component in Next.js

This ProjectList component showcases the power and flexibility of React Server Components. Unlike the Remix loader we saw earlier, this component encapsulates both the data fetching logic and the rendering in a reusable unit. It can be easily integrated into different parts of your application, demonstrating the granular nature of RSC.

The component handles authentication checks and data fetching, all on the server. This approach keeps sensitive operations (like database queries) on the server while sending only the rendered HTML to the client, enhancing both security and performance.

You can use this component in various parts of your application like this:

Components in various apps

This level of reusability is a key advantage of React Server Components. They allow you to create efficient, secure, and modular components that can be used throughout your application, blending seamlessly with client-side React components when needed.

By leveraging React Server Components, Next.js applications can achieve a fine balance between server-side and client-side rendering, optimizing for both performance and developer experience. The ability to perform data fetching and authentication at the component level, rather than just at the route level, offers developers more control over their application's behavior and performance characteristics.

Data Mutation

Data mutation is a critical aspect of web application development, determining how information is created, updated, and deleted within a system. In modern React-based frameworks, the approach to data mutation has evolved significantly, moving beyond traditional client-side updates to more efficient and reliable server-side methods. This section examines how Remix and Next.js handle data mutation, highlighting their unique approaches and the benefits they offer to developers in managing state changes and ensuring data consistency.

Remix Actions

In addition to loaders, each route in Remix can also have an action. Actions are server-side functions designed to handle data mutations, typically triggered by form submissions or other POST, PUT, PATCH, or DELETE requests. They allow you to perform server-side data modifications, such as updating a database or interacting with external APIs, before sending a response back to the client.

Remix leverages HTML forms as a core part of its data mutation strategy. This approach not only aligns with web standards but also provides a robust, JavaScript-independent way to handle user inputs and data submissions. By using native HTML forms, Remix ensures that your application remains functional even if JavaScript fails to load or execute, enhancing the overall reliability and accessibility of your web app.

An example of a typical action that any web app might have, that is dealing with sending an email for account activation:

673391a9ee9a9e90080b634c

In this example, we're utilizing a Remix action to handle account activation, demonstrating how server-side operations can be efficiently managed in a web application.

The action function receives a request object, which contains all the information about the incoming HTTP request. We use this to extract the form data submitted by the user, specifically their email address.

Once we have the email, we generate a JSON Web Token (JWT) with a 15-minute expiration time. and we interact with our database using the Drizzle ORM. We update the user's record, setting the newly generated activation token. This showcases how Remix actions can seamlessly integrate with database operations, allowing us to modify server-side state in response to user actions.

Finally, the action returns null. In Remix, actions must return something, but in this case, we don't need to use anything from the return value. If we wanted, we could return some piece of data about the operation to the client so that the client can display visual feedback to the user about the success of the mutation or something else.

Security and Validation

By handling these operations on the server, we ensure:

  • Sensitive operations like token generation are kept secure.
  • We can perform server-side validation before making any database changes.
  • Email sending is handled reliably without exposing sensitive SMTP credentials to the client.

The Client-Side Perspective

You might wonder, "How does the client know the operation was successful?" This is where Remix shines. After the action completes, Remix automatically revalidates the data on the page. This means the UI will update to reflect the new state without requiring a full page reload. The user gets immediate feedback, and the application remains responsive. This seamless integration between server-side actions and client-side updates is a key feature of Remix, enabling developers to create highly interactive and responsive web applications with minimal effort. Beside automatic revalidation, we can also take over control for some more complicated scenarios if the need arises.

Why This Approach is Effective

  1. Security: Sensitive operations are performed server-side, away from potential client-side vulnerabilities.
  2. Simplicity: The action encapsulates all the logic for account activation in one place.
  3. Performance: By handling everything server-side and using Remix's automatic revalidation, we avoid unnecessary API calls and keep the UI snappy.
  4. Scalability: This pattern can easily be extended to handle more complex workflows or additional account management features.

In conclusion, Remix actions provide a powerful, secure, and efficient way to handle server-side operations. They allow us to keep sensitive logic on the server while still providing a responsive and up-to-date user experience.

Since this action was triggered by a HTML form and executed on the server, the browser's network tab won’t show anything, but the UI will be fresh with new data.

While Remix's approach to data mutations and revalidation is powerful and elegant, it's worth noting one of its limitations: each route can only have a single loader function. This constraint, while promoting simplicity in many cases, can become a bottleneck in more complex scenarios.

The single loader per route design means that all data fetching for a given route must be channeled through one function. For routes with multiple, independent data requirements, this can lead to less modular and potentially less efficient code. Developers might find themselves cramming disparate data fetching logic into a single action, which can impact code organization and maintainability.

Interesting recent development: OpenAI has migrated ChatGPT’s frontend code from Next.js to Remix. However, they've opted not to utilize Remix's action functionality in their codebase.

Next.js Server Actions

While Remix actions are tied to routes and limited to one action per route, Next.js offers more flexibility with server actions. These can be triggered from various places where we typically initiate actions, such as button event listeners or forms.

Server actions provide a more granular approach to data mutations compared to Remix. They can be defined at a component level, promoting modularity and reusability in your codebase. This flexibility allows developers to structure their mutation logic more intuitively, often aligning closely with the UI components that trigger these actions.

Let's look at an example of how a server action would handle the previous case with user account activation:

673391fcee9a9e90080b63a0

The core functionality of this Server Action mirrors our Remix action, demonstrating the similarities in server-side processing between the two frameworks. However, there are some key differences in how Next.js approaches these operations:

  1. Flexibility in Definition: Unlike Remix actions which are tied to routes, Next.js Server Actions can be defined at the component level or in separate files, allowing for more granular control over where and how server-side logic is implemented.
  2. Explicit Revalidation: Notice the revalidatePath function at the end of our action. Unlike Remix's automatic revalidation, Next.js requires explicit calls to update the UI after a mutation. This gives developers more fine-grained control but also requires more careful management of data freshness.
  3. Progressive Enhancement: Like Remix, Next.js Server Actions support progressive enhancement, ensuring functionality even when JavaScript fails to load or execute.
  4. TypeScript Integration: When used with TypeScript, server actions provide end-to-end type safety from the server to the client, potentially reducing runtime errors.

Using Server Actions in Components

Server Actions can be used in other components like this:

67339275ee9a9e90080b63d7

As you can see, we're calling the Server Action from a client component. This seamless integration between server and client code is a powerful feature of Next.js.

Conclusion

Comparing Traditional BE+FE vs Full-Stack Approach

Full-stack frameworks like Remix and Next.js offer several advantages over traditional backend-frontend separation. They provide a unified codebase, shared typings, and seamless data flow between server and client, reducing development time and inconsistencies. Here’s a list of some pros and cons of full-stack framework approach:

Pros of Full-Stack Frameworks:

  1. Unified development experience
  2. End-to-end type safety
  3. Built-in performance optimizations
  4. Simplified deployment
  5. Rapid prototyping capabilities

Cons of Full-Stack Frameworks:

  1. Framework-specific learning curve
  2. Potential vendor lock-in
  3. Limited flexibility in technology choices
  4. Scalability challenges for monolithic apps
  5. Opinionated structure may not suit all teams

While traditional BE+FE setups offer more technological freedom and a clear separation of concerns, they can also lead to increased complexity and potential inconsistencies between layers.

The choice between these approaches depends on various factors that need to be carefully considered. Project requirements play a significant role, as do the expertise and experience of the development team. Scalability needs must be taken into account, especially for applications expected to grow significantly over time. Development speed priorities can also influence the decision, particularly in fast-paced environments with tight deadlines.

Long-term maintenance considerations are equally important, as the chosen approach will impact the ease of updates and bug fixes in the future. Both approaches have their merits, and the best choice ultimately aligns with the specific project goals and team dynamics.

As the landscape of web development continues to evolve, it remains crucial for developers and teams to stay adaptable while critically evaluating new paradigms to ensure ongoing success in their projects.

Note about React Server Components and Server Actions

It's important to note that server actions and React Server Components are not exclusive Next.js features, but rather core React innovations. These concepts were initially proposed by the React team to enhance server-client integration in React applications. Next.js, being at the forefront of React framework development, was among the first to implement and offer these features in a production-ready environment. However, as the React ecosystem evolves, we can expect to see these powerful capabilities adopted by other frameworks and toolchains, further blurring the lines between server and client in React applications across the board.

Comparing Remix and Next.js

While Next.js may seem more attractive on paper with its flexible Server Components and Server Actions, after working with both frameworks, I felt better working with Remix, and I opted to migrate my production app from Next.js to Remix. I’ll try to give a few personal reasons why and why I would prefer to work in Remix more:

  • React Server Components enable a lot of cool things and I get their goal and aim, but I found myself more comfortable with using Remix loaders because I knew exactly when I was on the server and when I was on the client. By using RSC and regular components, sometimes we may get lost in what is what, and nesting Server components with Client components is not really that straightforward, but it requires a few special tricks.
  • While Remix builds on time-tested server-centric patterns reminiscent of PHP and Ruby on Rails days, React Server Components represent an innovative approach that's still gaining traction in the global development ecosystem and is yet to prove itself as a better option.
  • With Remix, I could utilize all the good things React Router library offers, with how they handle redirects, route state, and not worrying to refresh my loaders after an action has been fired. I also had all the power of a web browser in every component, and all the power of Node.js in every loader/action. That felt like a clear separation of concerns and easy to reason about. next/navigation is also a great library for routing, but still not as good as React Router.
  • Biggest flaw for Remix in my book was having one action per route, which needed to have special code to manage, and I didn’t like that, but one can learn to live with it, and it may go away in the future.
  • This is based just on a feeling, I never did any benchmarks or anything, but Remix app felt a bit faster in the end, with much less code.
  • Remix uses Vite for build and Next.js still uses Webpack, or now super fast Turbopack, still Vite is the clear winner in the JS ecosystem and I would use it on any project.
  • Remix is a plain Node.js server and it can be deployed anywhere as such, for example I have used fly.io to deploy my app very easily. Next.js is by default a serverless app that best works with Vercel, and deploying it as a Node.js server requires some extra work.

I would still use any of these techs for a new project in a heartbeat and both are a safe bet for any project in the long run.

Quick comparison table between two frameworks:

Comparison between Remix and Next.js

Useful links for things mentioned in the post

Remix Official Website - https://remix.run

Next.js Official Website - https://nextjs.org

React Server Components - https://react.dev/reference/rsc/server-components

Next.js Server Actions - https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

Remix Loader - https://remix.run/docs/en/main/route/loader

Remix Action - https://remix.run/docs/en/main/route/action

Request Web API - https://developer.mozilla.org/en-US/docs/Web/API/Request

DrizzleORM (TypeScript database ORM used in the examples) - https://orm.drizzle.team

nodemailer (lib for sending emails using Node.js) - https://nodemailer.com

React Email (lib for creating beautiful email messages using React) - https://react.email

About the author

Milos Dzeletovic, a Software Engineer from Symphony's Belgrade branch, excels in frontend development and has strong expertise in HTML, CSS, JavaScript, and React.js. His strengths lie in his adaptability, self-driven learning, and proactive communication, which enable him to quickly adopt new technologies, effectively mentor peers, and contribute to building efficient, high-quality web applications.

Contact us if you have any questions about our company or products.

We will try to provide an answer within a few days.