#remix #web-development #framework #nested-routing #dynamic-segments #loader #action #SSR #hydration

In the world of making websites and apps, picking the right tool can make a big difference. Developers like us always look for tools that make our work easier and create great experiences for users. Whether you’re someone who’s been making websites for a while or you’re just starting out, Remix has something cool for you. Let’s start this journey and discover what makes Remix stand out.

This blog stands for Remix v1, for migration to v2, you can check here.

What is Remix.js

Remix, a recent full-stack framework built on top of React, offers a unique approach to server-side rendering (SSR) that enhances performance and minimizes JavaScript reliance. Unlike traditional setups where data is fetched on the frontend and rendered afterward, Remix shifts this process to the backend, serving pre-rendered HTML directly to users. With Remix, the backend and frontend synergize into a single cohesive unit, resulting in lightning-fast data rendering and an exceptional user experience.

Filesystem-based routing

Each route you define is seamlessly linked to a corresponding file residing within its designated routes directory. This crucial directory, properly positioned within the root App folder of your application, serves as the compass guiding Remix to automatically establish associations between routes and their respective files.

Folder Structure

Remix (v1) example folder structure

Before diving into details of routing, first let’s look at how Remix handles fetch & manipulate data on the server side.

Loader and Action Functions

In a Remix application, pages are dynamically rendered upon request using server-side rendering (SSR). However, Remix does not offer static site generation (SSG), which involves pre-building pages before deployment.

SSG creates web pages during the build process. visitors see the same content until the site is rebuilt. Usually ideal for content-focused sites with infrequent updates but can be problematic with dynamic content though because rebuilding the site on each change can be lasts long.

Otherwise Remix focuses on SSR. It generates the page’s HTML content on the server in response to the request. This means that the server processes the request, fetches data if needed, and generates the HTML markup for the requested page on the server side. Before rendering a page, Remix allows you to fetch data on the server side. This ensures that the data is available when the page is generated, which is especially useful for dynamic content. Let’s discover how Remix does this with loader and action functions.

Loader

As you can see, we do a simple fetch request from The Rick and Morty API. What we do is getting character data from the API.

import { useLoaderData } from '@remix-run/react'
import { json } from '@remix-run/node'
import type { LoaderArgs } from '@remix-run/node'

export type CharacterType = {
    /* You can implement your type here */
}

export async function loader({ request }: LoaderArgs) {
    const data: CharacterType = await fetch(
        'https://rickandmortyapi.com/api/character'
    ).then(async (res) => await res.json())

    return json(data)
}

export default function Blog() {
    const characters = useLoaderData<typeof loader>()
    console.log('characacters', characters)
    return (
        <div
            style={{
                fontFamily: 'system-ui, sans-serif',
                lineHeight: '1.8',
                display: 'flex',
                alignItems: 'center',
                flexDirection: 'column',
            }}
        >
            <h1>This is main blog page</h1>
        </div>
    )
}

Since Remix can fetch data on the server side also we can see the result on the terminal.

Output data on the terminal

Output data on the terminal

After Remix hydrate the app on the client, we can also see our data in the browser console.

Output data on the browser console

Output data on the browser

Action

Similar to loader, an action is a function that operates exclusively on the server and manages tasks like modifying data and performing other actions. Whenever a non-GET request (such as POST, PUT, PATCH, DELETE) is directed at your route, the action is executed prior to the loader.

Actions share the same API as loaders, differing only in their timing of execution.

This approach allows you to group all aspects related to a dataset within a single route module. This includes reading data, the component responsible for displaying the data, and making updates to the data.

Firstly, we can do a simple form submission:

import { Form, useActionData } from '@remix-run/react'
import { json } from '@remix-run/node'
import type { ActionArgs } from '@remix-run/node'

export async function action({ request }: ActionArgs) {
    const body = await request.formData()
    const name = body.get('userName')
    return json({ message: `Your name is ${name}` })
}

export default function Blog() {
    const data = useActionData<typeof action>()
    return (
        <div
            style={{
                fontFamily: 'system-ui, sans-serif',
                lineHeight: '1.8',
                display: 'flex',
                alignItems: 'center',
                flexDirection: 'column',
            }}
        >
            <h1>Submit your age</h1>
            <Form method="post">
                <div>
                    <label>Enter your name please.</label>
                    <input type="text" name="userName" />
                </div>
                <button type="submit">Submit</button>
                <p>{data ? data.message : 'Waiting...'}</p>
            </Form>
        </div>
    )
}
Simple form example of submitting name gif
Submit name

In this case we’re just checking if data exist, if not we’re showing a text on the screen. But it’s a bad practice since, it may cause confusion for users. Let’s do a more realistic example, how can we implement a real loading state when submitting (or getting) data. For that we were normally using useTransition hook, but since it’s deprecated, we are using the useNavigation hook for pending navigation indicators & optimistic UI on data mutations.

import { Form, useActionData, useNavigation } from '@remix-run/react'
import { json } from '@remix-run/node'
import type { ActionArgs, TypedResponse } from '@remix-run/node'

export async function action({ request }: ActionArgs) {
    const body = await request.formData()
    const name = body.get('userName')

    return new Promise<TypedResponse<{ message: string }>>((resolve) => {
        setTimeout(() => {
            resolve(json({ message: `Your name is ${name}` }))
        }, 2000)
    })
}

export default function Blog() {
    const data = useActionData<typeof action>()
    const navigation = useNavigation()
    return (
        <div
            style={{
                fontFamily: 'system-ui, sans-serif',
                lineHeight: '1.8',
                display: 'flex',
                alignItems: 'center',
                flexDirection: 'column',
            }}
        >
            <h1>Submit your name</h1>
            <Form method="post">
                <div>
                    <label>Enter your name please.</label>
                    <input type="name" name="userName" />
                </div>
                <button type="submit">Submit</button>
                <p>
                    {navigation.state === 'submitting'
                        ? 'Submitting'
                        : data?.message ?? null}
                </p>
            </Form>
        </div>
    )
}

We have created a copy of heavy API Request with timeout about 2 seconds. After 2 seconds promise will be resolved and return the data. With the help of the useNavigation hook, we can check the submission state, and render whatever when submission is on the fly.

The primary scenario where this hook is typically employed involves handling form validation errors. If the submitted form contains errors, you can throw errors or directly provide those errors to the user and allow them to make corrections.

export async function action({ request }: ActionArgs) {
    const body = await request.formData()
    const name = body.get('userName')

    if (typeof name !== 'string' || name.length < 3) {
        throw new Error('Invalid name!')
    }

    return json({ message: `Your name is ${name}` })
}
Submit invalid name gif

Submit invalid name

Routing

Understanding how routing works in Remix is really important. Routes serve as the foundation for various elements, including the compiler, the initial request, and a significant portion of subsequent user interactions.

Nested Routing

Another great idea of Remix is nested routes, which allow the framework to fetch data for multiple routes in parallel. For example, let’s assume our site has a page with the URL /blog to display a blog content:

import { Outlet } from '@remix-run/react'

export default function Blog() {
    return (
        <div
            style={{
                fontFamily: 'system-ui, sans-serif',
                lineHeight: '1.8',
                display: 'flex',
                alignItems: 'center',
                flexDirection: 'column',
            }}
        >
            <h1>This is main blog page</h1>
            <Outlet />
        </div>
    )
}

You can see the /blog page below:

/blog
/blog page

The <Outlet/> component in Remix works hand in hand with your folder structure to show the right content for different URLs. For instance, when someone visits /blog path, Remix uses the blog.tsx file to show the main content. If they go to /blog/example-blog path, it uses the example-blog.tsx file. Inside blog.tsx, you place <Outlet/> where you want nested content to appear, like the content from example-blog.tsx. It’s like a special marker that tells Remix where to put the content of different pages, making it easy to organize and display your website’s content as users explore different parts.

export default function ExampleBlog() {
    return <h3>This is example blog!</h3>
}

As you can see below, we have rendered our example-blog.tsx file as a child route.

/blog/example-blog

/blog/example-blog page

Dynamic Segments

We had already /projects route as mentioned example folder structure, what if we want to create dynamic segments under that route?

Prefixing a file name with $ will make that route path a dynamic segment. This means Remix will match any value in the URL for that segment and provide it to your app. Let’s check the /projects/$projects.tsx file.

import { useParams } from '@remix-run/react'

export default function Projects() {
    const params = useParams()
    const projects = params.projects
    return (
        <div
            style={{
                fontFamily: 'system-ui, sans-serif',
                lineHeight: '1.8',
                display: 'flex',
                alignItems: 'center',
                flexDirection: 'column',
            }}
        >
            <h1>This is {projects}</h1>
        </div>
    )
}

We are simply showing the a text with the projects parameter. The useParams hook as you can see above is a handy tool for working with dynamic segments in your URLs. It helps you access and use the values from these segments in your components. You can easily extract and utilize information from the URL to create dynamic and personalized content.

And the result as expected:

/projects/[slug]

/projects/project-1 page

Pay attention that we need to make sure that dynamic segment file name matches with params.[filename], otherwise you can not access any parameters.

Conclusion

In summary, Remix offers a distinctive approach to constructing web applications by seamlessly integrating server-side rendering and client-side interactivity. Its robust routing system ensures efficient navigation, while the loader and action functions facilitate effective data management on the server. By centralizing data fetching and rendering logic, Remix streamlines development and enhances performance, contributing to the creation of responsive and dynamic web experiences. As the landscape of web development evolves, Remix emerges as a tool that empowers developers to build modern, efficient, and user-centric applications. If you haven’t try Remix yet, I’d suggest you to give it a try.