@sudocode_

How i got sold on Remix

June 21, 2023 / 10 min read

Last Updated: June 21, 2023

For over 7 years, I've been immersed in the world of frontend development, constantly exploring different frameworks, tools, and libraries. I've hopped from Vue.js, Nuxt.js, and Gridsome to React.js, Next.js, and Gatsby.js, all in pursuit of the perfect framework that would elevate my skills as a developer.

During the past 5 years, React.js has been my primary focus. Within that timeframe, I've transitioned from Gatsby.js to Next.js and even dabbled in Blitz.js. Throughout my journey, developer experience (DX) has remained a top priority for me. I've yearned for a framework that would truly empower me and help me grow as a developer, and I believe I may have found it in Remix.

Although I primarily identify as a frontend developer, I've been eager to delve into backend development since 2019. However, I've often found myself daunted by the perceived complexity of backend development. It was during this time that I became deeply involved with Next.js, enticed by the allure of full-stack development.

Next.js allowed me to create impressive projects with ease, enabling the development of full-stack applications. Nonetheless, it had its own limitations, particularly in terms of DX. While it didn't hinder me significantly, I yearned for a framework that would truly empower and elevate my abilities as a developer.

Now, one will ask why I am so pumped about Remix. From Remix Blog: “one of the primary features of Remix is simplifying interactions with the server to get data into components.” Remix extends the flow of data across the network, making it truly one-way and cyclical: from the server (state), to the client (view), and back to the server (action).

Remix Data Flow

In Remix, you don't worry about using states as Remix keeps client-side state in sync with the server. You set mutation functions on the server via actions, and Remix will automatically prefetch up to-date data via the loaders and re-render the view.

Remix Data Flow

As seen in the picture above, the data flow is cyclical. The server sends data to the client via the loader, the client sends data back to the server via the action, and the action sync with the loader. This is the Remix data flow.

Working with Forms

In the client, the Form component imported from Remix is used to handle form submission. The Form component takes a method prop, which is the HTTP method to use when submitting the form. The Form component also takes an action prop, which is the URL to submit the form to. The Form component also takes a replace prop, which is a boolean that determines whether the browser history should be replaced or not.

working with forms

1
import { Form } from 'remix';
2
3
export default function Index() {
4
return (
5
<Form method="post" action="/login" replace>
6
<label>
7
Email
8
<input type="email" name="email" />
9
</label>
10
<label>
11
Password
12
<input type="password" name="password" />
13
</label>
14
<button type="submit">Login</button>
15
</Form>
16
);
17
}

Most times, you don't even need the action on the Form component if the action is in the same route as the Form component unless the Form component is in a different route or a resource route.

working with forms

1
import { Form, json, redirect } from 'remix';
2
3
// Action
4
export default async function action({ request }) {
5
const formData = await request.formData();
6
const email = formData.get('email');
7
const password = formData.get('password');
8
9
//check if email and password is valid
10
if (email === ' ' || password === ' ') {
11
return json({ message: 'Email or password is invalid' }, { status: 400 });
12
}
13
14
//send data to db or backend
15
const user = await db.users.create({ email, password });
16
return redirect(`/dashboard/${user.id}`);
17
}
18
19
// Component
20
export default function Index() {
21
return (
22
<Form method="post">
23
<label>
24
Email
25
<input type="email" name="email" />
26
</label>
27
<label>
28
Password
29
<input type="password" name="password" />
30
</label>
31
<button type="submit">Login</button>
32
</Form>
33
);
34
}

useFetcher is also a hook that is used to send data to the server. It takes a method argument, which is the HTTP method to use when sending data to the server.

working with forms

1
import { useFetcher } from 'remix';
2
3
export default function Index() {
4
const login = useFetcher();
5
6
return (
7
<login.Form method="post">
8
<label>
9
Email
10
<input type="email" name="email" />
11
</label>
12
<label>
13
Password
14
<input type="password" name="password" />
15
</label>
16
<button type="submit">Login</button>
17
</login.Form>
18
);
19
}

Handling Data on the Server

Now, it's the job of the action to handle the data sent from the client. The action is a server-only function to handle data mutations and other actions. If a non-GET request is made to your route (POST, PUT, PATCH, DELETE) then the action is called before the loaders.

Remix runs the action server side, revalidates data client side, and even handles race conditions from resubmissions.

handling data on the server

1
import { json, redirect } from 'remix';
2
3
export default async function action({ request }) {
4
const formData = await request.formData();
5
const email = formData.get('email');
6
const password = formData.get('password');
7
8
//check if email and password is valid
9
if (email === ' ' || password === ' ') {
10
return json({ message: 'Email or password is invalid' }, { status: 400 });
11
}
12
13
//send data to db or backend
14
const user = await db.users.create({ email, password });
15
return redirect(`/dashboard/${user.id}`);
16
}

Validate user input on the server and render validation errors in the client with ease thanks to useActionData.

Handling Errors on the Client

Errors can be displayed on the client via the useActionData hook. The useActionData hook returns the data sent from the server.

handling errors on the client

1
import { useActionData, useFetcher } from 'remix';
2
3
export default function Index() {
4
const data = useActionData();
5
const login = useFetcher();
6
7
return (
8
<div>
9
<p className="text-sm text-red-500">
10
{data.message && <p>{data.message}</p>}
11
</p>
12
<login.Form method="post" action="/login" replace>
13
<label>
14
Email
15
<input type="email" name="email" />
16
</label>
17
<label>
18
Password
19
<input type="password" name="password" />
20
</label>
21
<button type="submit">Login</button>
22
</login.Form>
23
</div>
24
);
25
}

oh and that's not all with Form in remix, there's a lot more.

Consuming Data from the Server

Most of the work we do on the client is consuming data from the server, Remix makes that even easier with the useLoaderData hook. This hook returns the JSON parsed from the route loader function.

consuming data from the server

1
import { useLoaderData } from 'remix';
2
3
export default function Index() {
4
const data = useLoaderData();
5
6
return (
7
<div>
8
<p>{data.message}</p>
9
</div>
10
);
11
}

Fetching Data from the DB

Since we know how to consume data from the server, let's see how to fetch data from the db or backend. The loader function only runs on the server. Remix calls this function via fetch from the browser and on the initial render on the server, provides data to the HTML document. loader is GET only, so it's not used for data mutations.

fetching data from the db

1
export default async function loader({ request }) {
2
const user = await db.users.find({ id: request.params.id });
3
return json({ user });
4
}

Streaming Data on the Server

Streaming data on the server is a great way to improve performance and user experience. This is done by utilizing the defer function from Remix. Defer is a utf-8 encoded JSON string that behaves just like json, but with the ability to transport promises to your UI components.

streaming data on the server

1
import { defer } from '@remix-run/cloudflare';
2
3
export async function loader({ request }) {
4
//not awaited, will stream in when ready
5
const users = db.users.find({ id: request.params.id });
6
7
//awaited, will load before rendering
8
const posts = await db.posts.find({ id: request.params.id });
9
return defer({ users, posts });
10
}

Optimistic UI

According to Remix Docs, Optimistic UI is a pattern to avoid showing busy spinners in your UI and make your application feel like it's responding instantly to user interactions that change data on the server. Even though it will take some time to make it to the server to be processed, we often have enough information in the UI that sent it to fake it. If for some reason it fails, we can then notify the user that there was a problem. In the vast majority of cases, it doesn't fail, and the app can respond instantly to the user's interactions. Read more on Optimistic UI.

Handling Client Errors

In Remix, each route module can export an error boundary next to the component that renders the UI. If an error is thrown, client or server side, users see the boundary instead of the default component.

handling client errors

1
export function ErrorBoundary({ error }) {
2
console.error(error);
3
return (
4
<div>
5
<h2>Oh snap!</h2>
6
<p>There was a problem loading this invoice</p>
7
</div>
8
);
9
}

If a route has no boundary, errors are caught by the error boundary in the parent route file which is the root.tsx.

At this point, I hope I have been able to convince you that Remix is a great framework for building web apps. If you are still not convinced, I will be sharing some resources that will help you get started with Remix.

Remix Stacks

Remix Stacks is a feature of the Remix CLI that allows you to generate a Remix project quickly and easily. There are several built-in and official stacks that are full-blown applications. There are few Stacks created by Remix, and you can check them out here but also, other developers have built their own stacks, and you can check them out on Github by Searching for remix-stacks.

Epic Stack by Kent C. Dodds is an opinionated project starter that allows teams to ship their ideas to production faster and on a more stable foundation. It comes with a lot of features out of the box, and it's a great stack to use if you want to build a web app with Remix.

Epic Stack Logos

Acccording to Kent, the primary goal of the Epic Stack is to help your team get over analysis paralysis by giving you solid opinions for technologies to use to build your web application. Read More on the Epic Stack.

Currently building La Ferme Victoire with Remix and Epic Stack.

Conclusion

If you're seeking a framework that can enhance your web development skills, I highly recommend giving Remix a try. In my experience, Remix has proven to be an exceptional framework that not only empowers developers but also encourages continuous learning. Instead of solely relying on their documentation, Remix redirects you to valuable resources like MDN and Web.dev, allowing you to expand your knowledge and discover new concepts that may have eluded you throughout your years as a developer. To delve deeper into Remix, I encourage you to explore the Remix Docs, where you can find comprehensive information and insights about this remarkable framework.

If you decide to learn Remix, you’ll organically also learn:

  • ArrowAn icon representing an arrow
    A lot about web fundamentals
  • ArrowAn icon representing an arrow
    Databases and ORM
  • ArrowAn icon representing an arrow
    API design
  • ArrowAn icon representing an arrow
    Authentication, Cookies, and Sessions
  • ArrowAn icon representing an arrow
    Caching
  • ArrowAn icon representing an arrow
    Testing
  • ArrowAn icon representing an arrow
    Performance
  • ArrowAn icon representing an arrow
    Continuous Integration and Deployment
  • ArrowAn icon representing an arrow
    And more!

Liked this article? Share it with a friend on Twitter or contact me let's start a new project. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll always be available to respond to your messages.

Have a wonderful day.

– Felix

One of the primary features of Remix is simplifying interactions with the server to get data into components.