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).
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.
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
1import { Form } from 'remix';23export default function Index() {4return (5<Form method="post" action="/login" replace>6<label>7Email8<input type="email" name="email" />9</label>10<label>11Password12<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
1import { Form, json, redirect } from 'remix';23// Action4export default async function action({ request }) {5const formData = await request.formData();6const email = formData.get('email');7const password = formData.get('password');89//check if email and password is valid10if (email === ' ' || password === ' ') {11return json({ message: 'Email or password is invalid' }, { status: 400 });12}1314//send data to db or backend15const user = await db.users.create({ email, password });16return redirect(`/dashboard/${user.id}`);17}1819// Component20export default function Index() {21return (22<Form method="post">23<label>24Email25<input type="email" name="email" />26</label>27<label>28Password29<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
1import { useFetcher } from 'remix';23export default function Index() {4const login = useFetcher();56return (7<login.Form method="post">8<label>9Email10<input type="email" name="email" />11</label>12<label>13Password14<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
1import { json, redirect } from 'remix';23export default async function action({ request }) {4const formData = await request.formData();5const email = formData.get('email');6const password = formData.get('password');78//check if email and password is valid9if (email === ' ' || password === ' ') {10return json({ message: 'Email or password is invalid' }, { status: 400 });11}1213//send data to db or backend14const user = await db.users.create({ email, password });15return 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
1import { useActionData, useFetcher } from 'remix';23export default function Index() {4const data = useActionData();5const login = useFetcher();67return (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>14Email15<input type="email" name="email" />16</label>17<label>18Password19<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
1import { useLoaderData } from 'remix';23export default function Index() {4const data = useLoaderData();56return (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
1export default async function loader({ request }) {2const user = await db.users.find({ id: request.params.id });3return 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
1import { defer } from '@remix-run/cloudflare';23export async function loader({ request }) {4//not awaited, will stream in when ready5const users = db.users.find({ id: request.params.id });67//awaited, will load before rendering8const posts = await db.posts.find({ id: request.params.id });9return 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
1export function ErrorBoundary({ error }) {2console.error(error);3return (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.
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:
- A lot about web fundamentals
- Databases and ORM
- API design
- Authentication, Cookies, and Sessions
- Caching
- Testing
- Performance
- Continuous Integration and Deployment
- 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.