Many programmers, including myself, have hobbies or side projects that are tech related. After hearing too much criticism towards my favorite framework (NextJS) I tried to use Remix in my new project. Remix acts as a good alternative to NextJS and Gatsby and does a good job in handling both client and server functionalities all in the same app. In this blog, I am going to share my findings on the easiest way to get started with Remix.
What is Remix
Remix is an edge-first server-side rendering JavaScript framework built on React that allows us to build full-stack web applications thanks to its frontend and server-side capabilities. On the frontend, it acts as a higher level React framework that offers server-side rendering, file-based routing, nested routes and a lot more, while on the backend it can be used to run a server of its own.
Unlike vanilla React, where data is fetched on the frontend and then rendered on the screen, Remix fetches data on the backend and serves the HTML directly to the user. It provides fully rendered pages on initial load, offering a quick and complete user interface experience.
[Remix is basically like] v7 of React Router. It’s like an upgrade of React Router that supports server rendering. It’s a compiler.
Kent C. Dodds
Remix vs Next.js vs Gatsby
Remix is without any doubt a a full-stack web framework. Similarly, Next.js is a React framework for building full-stack web applications, while Gatsby is a static site generator. Regardless of whether they’re similar or different in their definition, they are certainly different approaches to solving web development problems.
All of them try to solve one specific problem: avoid bloated SPA (Single Page Application) web applications and all their caveats. These frameworks try to be faster by delivering only the code that is essential to the browser: only what is needed for a page (HTML + JS + CSS + assets), only data needed for the context, and a better experience for SEO (search engine optimization). On top of that, they allow aggressive caching and CDN support.
Starting a New Remix Project
A Remix project can be started using either JavaScript or TypeScript. You can do a quick start, but the documentation suggests it’s more productive to start with a Remix template or stack. Templates allow you to bootstrap a minimum structure for your project
Installation
If you prefer to initialize a project without a template, you can use the CLI command. Alternatively, you can browse the list of community stacks on GitHub or the Remix Guide Templates list. But if you feel a bit lost, the Remix Official Stacks are a good starting point.
At the moment of writing this blog, I found that Remix has migrated to react router and so creating new remix apps no longer works.
🔄 Remix v2 is now part of React Router!
Remix v2 has been upstreamed into React Router and is now in maintenance mode.
For new projects, please use React Router instead.
The CLI lets you choose your bundler although the preferred one is vite since it is the fastest. You will also be prompted to choose your stack:
-
Choose your stack (TypeScript vs. JavaScript, Tailwind integration, etc.)
-
Example: selecting Vite + TypeScript + Tailwind (similar to your sample repo)
File Structure
Remix comes with a very simple file structure that makes things even easier for developers. Remix uses file-based routing unlike NextJS and other frameworks. At this stage, your folder structure should look something like this:
The file in app/root.tsx
is your root layout, or "root route". The "root" route (app/root.tsx
) is the only required route in your Remix application because it is the parent to all routes in your routes/
directory and is in charge of rendering the root <html>
document. You can use this file to render "document-level" components Remix provides. These components are to be used once inside your root route, and they include everything Remix figured out or built in order for your page to render properly.
Adding Your First Route
Any JavaScript or TypeScript files in the app/routes
directory will become routes in your application. The filename maps to the route's URL pathname, except for _index.tsx
which is the index route for the root route.
Adding a .
to a route filename will create a /
in the URL. For example:
Dynamic Segments
Usually your URLs aren't static but data-driven. Dynamic segments allow you to match segments of the URL and use that value in your code. You create them with the $
prefix.
Remix will parse the value from the URL and pass it to various APIs. We call these values "URL Parameters". The most useful places to access the URL params are in loaders and actions.
Server and Client Routes
Remix run unifies react router with api routes, ensuring that the same routes serve both UI and data. Unlike traditional react applications, where API handling is separate, remix integrates server and client logic. Files in the routes that end with .ts
are treated as api
routes. For example:
Loaders & Actions
One thing in Remix that is completely new is Loaders and Actions. Each route can define a loader
function that provides data to the route when rendering. Loaders help provide data to the component during rendering. This prevents race conditions where request resolution timing affects data consistency.
This function is only ever run on the server. On the initial server render, it will provide data to the HTML document. On navigations in the browser, Remix will call the function via fetch
from the browser.
This means you can talk directly to your database, use server-only API secrets, etc. Any code not used to render the UI will be removed from the browser bundle.
Action
A route action
is a server-only function to handle data mutations and other actions. If a non-GET
request is made to your route (DELETE
, PATCH
, POST
, or PUT
) then the action is called before the loader
s.
action
s have the same API as loader
s, the only difference is when they are called. This enables you to co-locate everything about a data set in a single route module: the data read, the component that renders the data, and the data writes:
When a POST
is made to a URL, multiple routes in your route hierarchy will match the URL. Unlike a GET
to loaders, where all of them are called to build the UI, only one action is called.
The route called will be the deepest matching route, unless the deepest matching route is an "index route". In this case, it will post to the parent route of the index (because they share the same URL, the parent wins).
Also note that forms without an action prop (<Form method="post">
) will automatically post to the same route within which they are rendered, so using the ?index
param to disambiguate between parent and index routes is only useful if you're posting to an index route from somewhere besides the index route itself. If you're posting from the index route to itself, or from the parent route to itself, you don't need to define a <Form action>
at all, omit it: <Form method="post">
.
Client Action
In addition to (or in place of) your action
, you may define a clientAction
function that will execute on the client.
Each route can define a clientAction
function that handles mutations:
This function is only ever run on the client and can be used in a few ways:
- Instead of a server
action
for full-client routes - To use alongside a
clientLoader
cache by invalidating the cache on mutations - To facilitate a migration from React Router
Client Loader
In addition to (or in place of) your loader
, you may define a clientLoader
function that will execute on the client.
Each route can define a clientLoader
function that provides data to the route when rendering:
This function is only ever run on the client and can be used in a few ways:
- Instead of a server
loader
for full-client routes - To use alongside a
clientLoader
cache by invalidating the cache on mutations- Maintaining a client-side cache to skip calls to the server
- Bypassing the Remix BFF hop and hitting your API directly from the client
- To further augment data loaded from the server
- I.e., loading user-specific preferences from
localStorage
- I.e., loading user-specific preferences from
- To facilitate a migration from React Router
Revalidating Data
When you use loader
to provide data to the client, the default behavior is for remix to cache the results so that no refetch is done when you reload the page. You might need to revalidate the data after mutations. To do this, you can take advantage of revalidator function from remix.
Alternatively, you can instruct Remix on when to revalidate your page using shouldRevalidate
function. This function lets apps optimize which routes data should be reloaded after actions and for client-side navigations.
During client-side transitions, Remix will optimize reloading of routes that are already rendering, like not reloading layout routes that aren't changing. In other cases, like form submissions or search param changes, Remix doesn't know which routes need to be reloaded, so it reloads them all to be safe. This ensures your UI always stays in sync with the state on your server.
This function lets apps further optimize by returning false
when Remix is about to reload a route. If you define this function on a route module, Remix will defer to your function on every navigation and every revalidation after an action is called. Again, this makes it possible for your UI to get out of sync with your server if you do it wrong, so be careful.
fetcher.load
calls also revalidate, but because they load a specific URL, they don't have to worry about route param or URL search param revalidations. fetcher.load
's only revalidate by default after action submissions and explicit revalidation requests via useRevalidator
.
Conclusion
In a world where complexity seems to take over all aspects of programming, it’s refreshing to have a tool that goes in the opposite way. Remix doesn’t try to embrace all use cases or to be a general solution. Instead, it aims to simplify the path for the well-defined problem of the web application.
Remix v2 is now part of React Router!
Remix v2 has been upstreamed into React Router and is now in maintenance mode. Remix does not recommend using the framework to create new apps.