What is Remix And How To Use It In Real Life
Bringing the bases back to life
Over the last year of web development history, one thing is clear, web applications are becoming more complex every day. The need of building complex and scalable applications in an easier way has allowed fantastic tools to emerge like React, Angular, Vue, etc, but nowadays, even with these tools in the game, developing large-scale web applications is challenging as we need to address different problems at the same time like state management, data fetching, styling, performance, and other complex tasks. That’s where Remix Run comes in.
Remix Run is a framework created to develop full-stack web applications, which provides a set of tools and abstractions over the web browser platform whose focus is helping us to create high-performant, accessible, and scalable sites.
The Real Problem with Modern Web Technologies
We mentioned before that tools such as React have allowed engineers to create many different types of web applications, which is awesome but, there is a problem hidden in the shadows. As the internet became massive, different kinds of users are reaching these websites and some concerns have emerged.
These concerns can be summed up as follows:
- We can control our servers, make them faster, and use the best technology on them, but we can not control the user’s network and the bottlenecks they create.
- Modern web frameworks work against the foundations of the web instead of working with them and, because of that, we need a lot of JavaScript code on the client side in order to run our applications, making the first point even worse.
- There are a lot of users whose connection needs to be improved to fetch the entire website in a decent amount of time. If our initial bundle is huge, this initial time will be huge indeed and the user experience will be diminished.
To solve these problems, Remix has brought the bases back to life by embracing the server/client model (including separation of source code from content/data.), working with the foundations of the web (Browsers, HTTP, and HTML), and using JavaScript to augment the user experience (so our sites will work without JavaScript as well).
What is Remix under the hood?
Quoting Remix’s documentation, Remix is the combination of four things:
- A compiler.
- A server-side HTTP handler.
- A server framework.
- A browser framework.
Under the hood, Remix uses ESBUILD as a compiler to create a server HTTP handler, a browser build with its assets, and an assets manifest. This way, we can deploy our application in any JavaScript hosting. This is the entry point of every Remix application.
We can say Remix works in a similar way to classic MVC web frameworks like Ruby on Rails but, it only implements the View and the Controllers leaving the Model up to you, so you are free to use any databases, ORMs, etc.
Instead of focusing on the Model layer, Remix focuses on the UI and introduces the Route Modules concept to take responsibility for the View and Controller layers.
If you are not familiar with MVC frameworks, another analogy you can use to understand Remix is:
Remix Route Modules are React Components with their own API route and know how to talk themselves on the server to load and submit data.
To make a Route Module to work Remix has implemented export functions on it: the most important ones are loader
, action
, and default
(the UI component).
We can see an example here.
export const loader = async ({ params }: LoaderArgs) => {
const post = await getPost(params.slug);
return json({ post });
};
export async function action({ request }: ActionArgs) {
const form = await request.formData();
const errors = validate(form);
if (errors) {
return json({ errors });
}
await createPost({ title: form.get("title"), content: form.get("content") });
return json({ ok: true });
}
const PostSlug = () => {
const { post } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
return (
<main className="mx-auto max-w-4xl">
<h1 className="my-6 border-b-2 text-center text-3xl">
Post title: {post?.title}
</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<Form method="post">
<input name="title" />
<textarea name="content" />
<button type="submit">Create New Project</button>
</Form>
{actionData?.errors ? (
<ErrorMessages errors={actionData.errors} />
) : null}
</main>
);
};
export default PostSlug;
The loaders
run only on the server and are used to provide data to your UI components on GET requests. The actions
run on the server but are used to handle POST, PUT, PATCH, and DELETE.
Additionally, the majority of the app works before JavaScript loads in the browser, which makes Remix apps resilient to network conditions by design.
Remix serves the document to the browser and then it hydrates the page with the browser build’s JavaScript modules. This way Remix emulates the browser.
Remix has some built-in optimizations for client-side navigation and also exposes some React Components and Hooks as part of the client-side APIs which we can use to create rich user experiences without changing the fundamental model of HTML and browsers.
This is why we say Remix is built on top of React JS.
The best part is that they are available just out of the box
Enough theory, let’s get our hands dirty with some code.
How to use Remix in a Real Project
Let’s get started by creating a Remix Project. For this purpose, Remix provides us with a developer CLI tool and different tech stacks (official stacks come ready with common things you need for a production application).
In our case, we are going to start our application from scratch but I strongly recommend you use one of the stacks.
You can read more about the CLI here and, the tech stacks here.
To generate a new Remix project we use remix-create
. Open your terminal and run this command:
$ npx create-remix@latest
To use one of the tech stacks use --template
flag or use the interactive CLI menu.
Once the setup script has run, you will be asked a few questions to create the project.
? Where would you like to create your app? remix-demo-project
? What type of app do you want to create? Just the basics
? Where do you want to deploy? Choose Remix App Server if you're unsure; it's easy to
change deployment targets. Remix App Server
? TypeScript or JavaScript? TypeScript
? Do you want me to run `npm install`? Yes
Let’s open the created directory and explore what we have. In my case, I named as remix-demo-project
. You should see a tree structure like this one:
remix-demo-project
├── README.md
├── app
│ ├── entry.client.tsx
│ ├── entry.server.tsx
│ ├── root.tsx
│ └── routes
│ └── index.tsx
├── package-lock.json
├── package.json
├── public
│ └── favicon.ico
├── remix.config.js
├── remix.env.d.ts
└── tsconfig.json
The app directory is where your Remix app lives. Inside you can find the root.tsx file which is our root component where we have to render the `<html></html>` element alongside other components that Remix provides us out of the box.
//root.tsx
const App() = () => {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
export default App;
We can find another two files here. The file in charge of hydrating our React components once the app loads in the browser app/entry.client.tsx
and the file in charge of rendering our React app on the server and sending it as a response app/entry.server.tsx
.
Last but not least, we also have a package.json
with all the available scripts to run, build and deploy our application.
Let’s run our application in development mode so we can start playing around with it.
First, delete the content of the route folder so we can really start from scratch, and then run the following command.
$ npm run dev
Remix App Server started at http://localhost:3000 (http://192.168.1.173:3000)
First of all, let’s explore the Routing system as it is the most fundamental block in every Remix application.
Routing in Remix
Routing is the most important concept in Remix as everything in Remix start with your routes.
Full Documentation is available here.
The most common way to create a route in Remix is through the file system. This is called file-based routing. To create a route we need to create a file inside app/routes
directory.
Let’s start by creating our root route. To do this, create a file called index.tsx
inside app/routes
and export a React Component by default.
// app/routes/index.tsx
const Root = () => {
return (
<div>
<h1>This is the route we should see at /</h1>
</div>
);
};
export default Root;
Then in your browser, you should be able to see this.
If you want to create other pages like /products or /cart you can follow the same convention and create a file inside app/routes with the route name.
After creating that file you should be able to go to /products
in your browser and see the page.
Now let’s create a page to display specific products at /products/$productId
. To do this, Remix allows us to use parametrized routes following the same approach, but this time, we are going to use another concept which is nested routing.
Nested Routing is the general idea of coupling segments of the URL to component hierarchy in the UI.
First, let’s modify our page for available products.
import { Link, Outlet } from '@remix-run/react';
const Products = () => {
return (
<div>
<h1>List of Products</h1>
<hr />
<ul>
<Link to='product1'>
<li>Product 1</li>
</Link>
<Link to='product2'>
<li>Product 2</li>
</Link>
<Link to='product3'>
<li>Product 3</li>
</Link>
</ul>
<main>
<Outlet />
</main>
</div>
);
};
export default Products;
Note that we have included the <Outlet />
component available in the @remix-run/react
package, this will be our placeholder for children routes.
Now create two new files inside app/routes/products/
as follows:
// app/routes/products/index.tsx
const NoProducts = () => {
return <div>No Products</div>;
};
export default NoProducts;
// app/routes/products/$productId.tsx
const Product = () => {
return (
<div>
<h3>Product Details</h3>
</div>
);
};
export default Product;
Note the $
in front of the file name and its location. This is to tell Remix that this file is nested inside /products
the route and it’s parametrized by productId
.
Now if you go to /products
you should see something like this.
We can see NoProducts
component is displayed instead of our <Outlet />
.
But if you click on any link, you’ll see the URL changes and in place of the <Outlet />
component, we’ll see our ProductDetails
. That’s how nested and parametrized routes work. Now let’s display some information according to the selected product by modifying app/routes/products/$productId.tsx
component.
To create a more real-world example, I’m going to use some dummy data (a JSON file) for the product details and introduce a new concept which is Data Fetching.
Data Fetching in Remix
If you remember in the introduction we mentioned that in Remix your frontend component is also its own API route, and it knows how to talk to itself on the server from the browser.
To fetch data you need to export a loader
function from your route module. Inside this loader
you write your logic to fetch the data (remember this runs on the server) and return the data to be displayed.
Let’s start by creating a loader
in our products page to fetch our products and use useLoaderData
to use the data in our UI component.
// app/routes/products.tsx
import { json } from '@remix-run/node';
import { Link, Outlet, useLoaderData } from '@remix-run/react';
import { fetchProducts } from '~/utils/fetchProducts';
export const loader = async () => {
const products = await fetchProducts();
return json({ products });
};
const Products = () => {
const data = useLoaderData<typeof loader>();
return (
<div>
<h1>List of Products</h1>
<hr />
<ul>
{data.products.map((product) => (
<Link to={product.link} key={product.link}>
<li>{product.title}</li>
</Link>
))}
</ul>
<main>
<Outlet />
</main>
</div>
);
};
export default Products;
And now let’s modify our $productId.tsx to display the product details using the same approach.
// app/routes/products/$productId.tsx
import type { LoaderArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { fetchProductDetails } from '~/utils/fetchProducts';
import { useLoaderData } from '@remix-run/react';
export const loader = async ({ params }: LoaderArgs) => {
const product = await fetchProductDetails(params.productId!);
return json({ title: product.title, description: product.description });
};
const Product = () => {
const data = useLoaderData<typeof loader>();
return (
<div>
<h3>{data.title}</h3>
<p>{data.description}</p>
</div>
);
};
export default Product;
Note that we don’t need to return the entire object. Instead, we can return just what we need and that means less code delivered to clients as we said at the beginning.
As always, I encourage you to check the documentation for each section, here you have a full explanation of how to fetch data.
Now let’s explore a way we can write data on the server so we can add new products.
Mutations in Remix
Let’s create a form to add new products to our page. To do that, create the component page at app/routes/products/new.tsx to include a form to enter new products. The way to write data on the servers in Remix is through actions and as we did with loaders we need to export the function from out route module.
import type { ActionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useActionData } from '@remix-run/react';
import { createProduct } from '~/utils/productUtils';
export const action = async ({ request }: ActionArgs) => {
const body = await request.formData();
const title = body.get('title') as string;
const description = body.get('description') as string;
const success = await createProduct(title, description);
return json({ ok: success });
};
const NewProduct = () => {
const data = useActionData<typeof action>();
console.log({ data });
return (
<form method='post'>
<input type='text' name='title' defaultValue='' />
<input type='text' name='description' defaultValue='' />
<button type='submit'>Add Product</button>
</form>
);
};
export default NewProduct;
After creating the new product we should be able to see it in our list.
Note: as we don’t have a data persistency layer, once we refresh the page, we’re going to lose the new product. Remix is capable of interact with a Database but that’s out of the scope of this tutorial.
Now our little app looks ugly, so I think is a good time to introduce the styling in Remix.
Styling in Remix
To add styles in Remix we need to add a <link rel="stylesheet">
to the page we are styling and, as we did with actions
and loaders
we can include these links in a Route Module by exporting a link
function.
First, we need some basic styles. I’m going to create these in a separate folder app/styles
.
// app/styles/index.css
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.header {
background-color: #ccc;
padding: 8px;
}
And then use them in our link function inside our index component. After that you should be able to see our basic styles applied in our index page.
import type { LinksFunction } from '@remix-run/node';
import styles from '~/styles/index.css';
export const links: LinksFunction = () => {
return [{ rel: 'stylesheet', href: styles }];
};
const Root = () => {
return (
<header className='header'>
<h1>This is the route we should see at /</h1>
</header>
);
};
export default Root;
The interesting thing here is, if we go to our products page and, inspect it, no CSS was imported. That’s awesome because it means we don’t need to import CSS that’s not going to be used at all, making our page load really fast while avoiding some class conflicts.
If you want to learn more about styling you can read it here. But we have covered the basics.
Conclusion
At this point, we have covered the basics of Remix Run, what it is, what it does, and, why it was created in the first place. I really encourage you to go and check the documentation and try out some projects on your own.
To Recap, the true power of Remix lies in the concept of Route Module, which is a file where we can implement all the functionalities we need to create our web applications such as data fetching, mutations, styling, etc, by exporting functions.
These Routes Modules must be created inside the routes directory and there we can use different components and hooks that Remix gives us just out of the box.