Editor’s note: This article was last updated on 15 December 2022 to include sections on deployment and error handling.
Remix, a full-stack web framework from the creators of React Router, has transitioned from a paid to a free model, which is big news within both the React and the greater meta-framework communities.
Within the past few years, the usage of the SaaS paradigm, the business model typically used by open source technologies in the cloud, has been solidified within the industry. For example, React meta-frameworks Next.js and Gatsby both offer paid hosting services with additional features that are tailored for optimization.
Similarly, Shopify released a React meta-framework called Hydrogen as well as a hosting service for it called Oxygen. Databases like Neo4j, ArangoDB, and MongoDB each offer cloud database services that make adoption and usage easier. Eventually, the Remix creators have plans to release an individualized, optimized platform as well.
Meanwhile, Vercel, the creators behind the Remix competitor Next.js, has had an interesting development in hiring Svelte creator, Rich Harris, to work full-time on SvelteKit, the primary Svelte meta-framework.
As a framework for server-side rendering, Remix aims to fulfill some of the same needs as frameworks like Next.js and SvelteKit. In this article, we’ll compare a few of their features, ranging from initiating a project to adding styling. At the end of this article, you should be better equipped to select one for your unique project. Let’s get started!
N.B., the equivalent meta-frameworks for Vue, Angular, and SolidJS would be Nuxt.js, Angular Universal, and SolidStart, respectively.
First, we’ll consider the commands for creating a new project in each framework:
//Remix npx create-remix@latest
When generating a Remix project, you can select a template tailored for deploying to different hosting services, like Netlify, Cloudflare, Fly.io, Vercel, and more. Each comes equipped with the documentation to make deployment a breeze:
//Next.js npx create-next-app@latest //SvelteKit npm init svelte@next my-app
Routing determines what URL you’ll need to access different pages on the website. All three meta-frameworks use file-based routing, which is primarily what all meta-frameworks use. The URL is based on the name and location of the file for that particular page.
Below, you’ll see some examples of how different files get mapped to URLs in each meta-framework, including an example with URL params, which define a part of the URL as a variable that you can retrieve.
Remix is built on top of React Router v6. Therefore, you can use many of the newest Hooks, like useParams
and useNavigate
in your client-side code to handle navigation, similar to using React Router v6 normally:
/
→ app/routes/index.js
/hello
→ app/routes/hello/index.js or app/routes/hello.js
/use_param/:id
→ app/routes/use_param/$id.js
/
→ pages/index.js
/hello
→ pages/hello/index.js
or pages/hello.js
/use_param/:id
→ pages/use_param/[id].js
/
→ src/routes/index.svelte
/hello
→ src/routes/hello/index.svelte
or src/routes/hello.svelte
/use_param/:id
→ src/routes/use_param/[id].svelte
A major benefit of using a meta-framework is handling a lot of data preparation prior to your page hydrating, like API calls, transformations, etc. When you use a meta-framework, you don’t have to prepare loaders or things like the useEffect
Hook to deal with the asynchronous nature of these issues.
In all three meta-frameworks, there is a function on each page that we can define, which will be run from the server prior to shipping the page to the user’s browser.
In Remix, we define a function called loader
, which will be passed an object with things like URL params and the request data that we’ll use to prepare any data. The loader
function can then return any data that the page will need. This data can then be retrieved in the component using the useLoaderData
Hook as follows:
import { useLoaderData } from "remix"; export let loader = async ({ params, request }) => { // get a param from the url const id = params.id // getting data from the url query string const url = new URL(request.url) const limit = url.searchParams.get("limit") return {id, limit} }; export default function SomePage() { let {id, limit} = useLoaderData(); return ( <div> <h1>The params is: {id}</h1> <h1>The limit url query is: {limit}</h1> </div> ); }
Similarly, in Next.js, you can export a function called getServerSideProps
. The return value can then define the props to the page
component:
export const getServerSideProps = async ({ params, query }) => { // get a param from the url const id = params.id // getting data from the url query string const limit = query.limit return {props: {id, limit}} }; export default function SomePage() { let {id, limit} = useLoaderData(); return ( <div> <h1>The params is: {id}</h1> <h1>The limit url query is: {limit}</h1> </div> ); }
With SvelteKit, you define a function called load
in a separately designated script block. Just like the previous examples, you can handle any API calls and data preparation, then return the data to be used as props to the page
component:
<script context="module"> // Load function to define data export function load({ page }) { // get params from url const id = page.params.id // get data from url query const limit = page.query.get("limit") return { props: { id, limit } }; } </script> <script> // normal client side javascript block export let id; export let limit </script> <div> <h1>The params is: {id}</h1> <h1>The limit url query is: {limit}</h1> </div>
Pre-rendering pages as static site generators is probably the biggest diversion in feature sets. At the time of writing, Remix does not support pre-rendering of pages, while Next.js and SvelteKit do, meaning you can also use them as static site generators.
At the time of writing, Remix does not support static site generation, but it provides a guide on using distributed cloud technologies to optimize your app.
If you prefer that your page be pre-rendered, simply export getStaticProps
instead of getServerSideProps
. Otherwise, we’ll observe the same pattern as before:
If you want your page to be pre-rendered in the module script blog, just set the following code:
export const prerender = true;
The code above will tell SvelteKit to pre-render the page instead of rendering it on each request.
While we can handle logic on the server side with loader
, getServerSideProps
, or the load
function, API keys and other data shouldn’t be in this code. You may still need dedicated API URLs with code that is only visible and run on the server side.
If you create a file that doesn’t export a component, then it will be treated as a resources route that can create a JSON response as follows:
export function loader({ params }) { const id = params.id return new Response({id}, { status: 200, headers: { "Content-Type": "application/json" } }); }
If you create a route that exports a route function, like in Express within the pages/api
folder, it will be treated similarly to an API route:
export default function handler(req, res) { res.status(200).json({ id: req.params.id }) }
If you have a JavaScript or TypeScript file instead of a Svelte file, export a function, and it will be treated as an API route. The name of the function will determine what method it is a response to:
export async function get({ params }) { return { body: {id: params.id} } }
When it comes to styling, the three frameworks can differ quite a lot.
Remix has a built-in way of linking traditional CSS style sheets via link tags by exporting a link function in the pages
JavaScript file. If a link tag is present in the root or the template, the page’s link tag will be inserted afterward. Therefore, you don’t need to have all of your CSS present on every page to optimize how much CSS you’re sending per page:
export function links() { return [ { rel: "stylesheet", href: "https://unpkg.com/[email protected]/dist/reset.min.css" } ]; }
You can use the helmet
component to add link tags as well, but you can also use styled-components, JSS, Emotion, Sass, or any other CSS abstraction along with just importing standard CSS style sheets.
Like Vue, Svelte uses single-file components, so the CSS is in the components file.
Despite your best efforts to ship applications that are highly performant and bug-free, errors are bound to occur. The web does not run on your localhost
, hence the need to prepare for these errors and handle them if and when they do pop up.
Remix has error-handling support for server and client rendering baked into it. The error boundaries don’t block the entire page from rendering because they only replace the part of the view where an error occurs.
Errors in Remix bubble up the component tree until they find an error boundary to render them:
import { ErrorBoundaryComponent } from "remix"; export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => { return ( <div> <p>An error occured!</p> <pre className="mt-4">{error.message}</pre> </div> ); };
To handle errors, Next.js uses React’s ErrorBoundary
component:
class ErrorBoundary extends React.Component { constructor(props) { super(props) // Define a state variable to track whether is an error or not this.state = { hasError: false } } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI return { hasError: true } } componentDidCatch(error, errorInfo) { // You can use your own error logging service here console.log({ error, errorInfo }) } render() { // Check if the error is thrown if (this.state.hasError) { // You can render any custom fallback UI return ( <div> <h2>Oops, there is an error!</h2> <button type="button" onClick={() => this.setState({ hasError: false })} > Try again? </button> </div> ) } // Return children components in case of no error return this.props.children } } export default ErrorBoundary
The ErrorBoundary
component keeps track of a hasError
state variable, which is a boolean. The ErrorBoundary
component will render a fallback UI if there is an error, and if not, it will render its children.
Unlike Remix and Next.js, SvelteKit does not have any form of ErrorBoundary
component to handle errors. Rather, you have to handle errors by creating a +error.svelte
, which will catch and render the error.
Error objects in Sveltekit contain a message
property with a string
value that provides more details on the nature of the error.
You can create errors using the error
helper imported from @sveltejs/kit
. Consider a scenario where you want to fetch an article’s details, but they were not found:
export async function load({ params }) { const post = await db.getPost(params.slug); if (!post) { throw error(500, { message: 'An error occured!' }); } return { post }; }
The +error.svelte
component will automatically catch that error and display it:
<script> import { page } from '$app/stores'; </script> <h1>{$page.error.message}</h1>
Several platforms like Fly, Vercel, Render, Netlify, and Cloudflare provide walkthrough guides that cover how to deploy Remix applications. Remix also provides several templates you can utilize for quick deployment using the CLI.
Vercel, the creators behind Next.js, is usually the first option for deploying Next.js apps. Next.js provides more deployment flexibility because it can be deployed to any hosting provider that supports Node.js. Some of these hosting providers include Netlify, Cloudflare, Render, AWS, and Heroku. Note that Next.js has more deployment options than SvelteKit and Remix.
Vercel and Render provide direct, first-class support for quickly deploying SvelteKit applications from GitHub, GitBucket, and GitLab. For other platforms, you can utilize SvelteKit’s Adapters API to optimize for deployment. Netlify and Cloudflare provide guides on how to deploy SvelteKit applications with adapters.
Remix truly has a unique and valuable difference in the way it handles forms. In modern frameworks, we’ve abandoned the traditional functionality of forms in place of hijacking the process from within JavaScript. For those who developed web applications long ago, you probably remember forms that look like this:
<form method="post" action="/user/new"> <input type="text" name="username"> <input type="password" name="password"> <input type="submit" value="new user"> </form>
Both the request method and the place where we made the request were entirely defined in the form, so there’s no need for onSubmit
handlers or preventDefault
. In our case, it worked because we had Perl or PHP scripts waiting on the other end to handle that request. Remix has a custom Form
component that embraces the feel of this type of experience.
Below is a sample component from the Remix documentation illustrating the Form
component in action:
import type { ActionFunction } from "remix"; import { redirect } from "remix"; // Note the "action" export name, this will handle our form POST export const action = async ({ request }) => { const formData = await request.formData(); const project = await createProject(formData); return redirect(`/projects/${project.id}`); }; export default function NewProject() { return ( <form method="post" action="/projects/new"> <p> <label> Name: <input name="name" type="text" /> </label> </p> <p> <label> Description: <br /> <textarea name="description" /> </label> </p> <p> <button type="submit">Create</button> </p> </form> ); }
When the form is submitted, it will make a POST
request to /projects/new
. The form will use that exported action function for handling, then redirect to the proper route. What’s old is new again!
If you want to bring some of that old-school, full-stack web application feel, but still capture the benefits of React on the client side when needed, Remix is a great choice for your meta-framework.
Although the lack of static pre-rendering makes it difficult to use for certain use cases, like blogs and marketing funnels, Remix is a strong addition to the toolkit for sites with lots of dynamic content.
One thing is for sure, the place to look for innovations right now in the frontend framework space is really in the meta-frameworks, like Remix, Next.js, SvelteKit, Nuxt.js, Gatsby, SolidStart, Gridsome, etc. We are even entering an era where meta-meta-frameworks exist. I’m looking at you, Blitz.js, built on Next.js.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
2 Replies to "Remix vs. Next.js vs. SvelteKit"
Hey, I’d want to notify that the NextJS example about “loading data on the server” isn’t correct. In the “SomePage” component, you’re using a Remix hook “useLoaderData” to receive the data instead of using page props.
Great Content!!
Used one of your articles to implement Google authentication on my NextJS app and it worked wonders.