New Freimeworks in the React ecosystem - Remix

New Freimeworks in the React ecosystem - Remix

In the React.js ecosystem, Next.js has especially established itself as the foundation for creating dynamic websites. It doesn't matter what field a site belongs to - blog, e-commerce, entertainment portal: if it's written in React, it's likely to use Next.js under the hood. At the end of 2021, there's a new framework on the market from the creators of React Router: Remix. I'll give a brief overview of Remix and compare it to Next.js.

Remix is based on server-side rendering of websites and is very easy to develop, has high page load speed, requires minimal code, and is characterized by an innovative approach in routing. Let's take a closer look.

Remix can be installed using npm, as you would normally do with any JavaScript framework. With npx create-remix, a new project is simply created. The script asks the user for preferences such as whether to use TypeScript or not, the preferred server implementation (Express, Vercel, Netlify, etc.), and creates a minimal project framework. Remix also offers more complex templates that feature preconfigured stacks with a database solution, integration and unit tests, a deployment pipeline, and integration with various hosting providers.

Server-Side Rendering

At the heart of Remix is Server-Side Rendering. But what does it really mean? Rendering means filling an HTML document with content. Basically, a distinction is made between client-side and server-side rendering.

For example, if you create a one-page React application with create-react-app and make it available on a hosting site, then when you access the site, an HTML document will be loaded. However, the contents of this document are limited to links to the styles and scripts needed to build the site. HTML structures are only built in the browser, so we can talk about a client-side rendering (CSR) site. The important point here is that usually all the routes of a CSR-based site are defined by the same HTML file. This means that search engine robots see the same metadata for all routes. This is bad, for example, in the case of an online store, because separate mappings are not created for different product pages.

If a web server fills an HTML document with content before displaying it to the user, this is called server-side rendering (SSR). Since documents are created dynamically, the aforementioned metadata problem does not occur. However, it takes longer for a website to respond because the server has to build the response first and cannot just fetch it directly from memory as in the case of a static host.

In addition to CSR and SSR, there is also Static Site Generation (SSG). In this case, all site routes are pre-rendered and the HTML documents are uploaded to the host. This approach can be interesting if the site contains few static routes, such as a portfolio or documentation site. For an online store with hundreds of products that can be edited on the fly, this method is not suitable.

Solutions from Remix

Remix, as a framework, runs the server that powers the React application. When a route is invoked, the Remix server renders the HTML document before passing it to the user. This way, the browser can display the site immediately. JavaScript is executed in the background, effectively turning the site into a single-page client-side application. The stage where the client takes over the rendering is called hydration. This gives Remix the benefits of SSR, such as dynamic metadata and faster display of content in the browser, as well as the benefits of CSR - making changes to the site without reloading the HTML document.

One of the problems that arise when using server-side pre-rendered web pages with client-side hydration is that often the page is already rendered to the user before hydration, but is not yet interactive. Remix solves this problem by building the data concept on HTML forms.

Routing

Remix uses files and directories to define routing. All JavaScript module files stored in the app/pages directory are interpreted by Remix as route components. The file name defines the route segments. For example, a module in the app/pages/products.tsx directory defines the /products route. For deeper routes, subfolders can be created. Or in newer versions, file names where paths are separated by dots. Variables can be defined in a filename using the $ sign. For example, a file defined in app/pages/products.$productId.tsx is included in all products/* routes.

Routing files export a regular React component using the default export. Features provided by React, such as hooks, can also be used here. In addition to the default export, other optional exports can be provided that can be used for data querying, error recovery, and metadata declaration.

An interesting feature of routing in Remix is nesting. Remix assumes that sites are generally hierarchical. For example, all site routes have the same navigation bar, account settings have sub-navigation, or all product pages have the same layout. Therefore, the roots stored in the app/pages section do not define all routes, but only segments of them, as shown in the image for the post. This shows a product site assembled from three segments (root component, $product-Id.tsx and $variantId.tsx) and how the invoked URL maps to the components of the route.

Nesting is provided by the <Outlet/> component provided by Remix. It can be included in the segment rendering function. When segments are assembled, sub-segments are included.

When rendering a route, Remix starts at the root in app/root.tsx and binds the next route segment to the <Outlet/> component. The next segment is then included in the previous component. This continues until the entire URL has been assigned.

Now the question may arise as to how routing works with index pages. If, for example, you want to list all categories in the /categories route, the obvious thought would be to put this list in app/routes/categories.tsx. However, this would cause more specific routes, such as /categories/123, to also display this list. In Remix, this problem is solved by using index segments. In the app/routes folder, you can create a file named categories._index.tsx. Remix will then favor this index segment if it is the last element of the path.

The individual segments are visualized in parallel and isolated from each other and are collected as soon as all segments are ready. Because of the isolation, data between path segments cannot be shared. This means that they can be queried multiple times. However, the advantage of this strategy is that if an error occurs in one segment, it can be guaranteed that the other segments will continue to operate. This is what Remix builds its error recovery strategy on. In a route segment, you can export the errorBoundary.

As with the default export, the functions are React components. When an error occurs, the contents of these React functions are displayed instead of the actual content. Because the segments are independent of each other, everything above them remains interactive. If errorBoundary is not defined, Remix climbs up the segments to the root until the errorBoundary is found.

Data Flow

An important part of any website is data. A web store, blog, or news site without data is... well, well, it's not worth reading. Remix implements data flow integrated with routing. Each segment can contain a loader and an action. These functions, unlike errorBoundary, are not JSX components, but return a Response. Loader and action can be compared to query and mutation, which are known from GraphQL.

Loader are read operations, such as requesting product information or a blog post. Actions, on the other hand, are write operations. They are used to log in, create blog entries, or post comments - basically, wherever the state returned by the loader function changes.

The data flow can be represented as a triangle. The corners represent the render, loader, and action functions. When the user calls the page, the loader first provides the necessary data. This data is passed to the render function. This function can then define the form in which the action is called. It changes the state on the server, which also changes the state of the loader, which in turn causes the rendered content to change.

Importantly, the loader and actions are not included in the client JS bundle. Every call to these functions happens on the Remix server. This also means that the function does not have access to browser APIs such as localStorage or sessionStorage. If you need to store data between the browser and the server, it is recommended to use a session cookie. Remix offers a cookie implementation that stores data as a base64 encoded string.

The Loader should return a response object. Response is part of the Node.js API and describes an HTTP response. Response allows you to return any content and set all headers. With these headers, Remix also allows, for example, redirects or session cookies.

Remix offers several helper functions that abstract Response, such as the json function. The data returned by the loader function can be used in the rendering function using the useLoaderData hook.

 

export const loader = () => {

  return json({status: "ok"})

}

 

This function is identical to the function below:

 

export const loader = () => {

  return new Response(JSON.stringify({status: "ok"}), headers: {"Content-Type": "application/json; charset=utf-8"})

}

 

The loader can be used as follows:

 

export default function ExampleSegment() {

  const {status} = useLoaderData();

  return <div>{{status}}</div>

}

 

However, data transfer is usually required as well. For example, logging in without the ability to pass a username and password is not very useful. For this purpose, an action is used. Actions are defined in the same way as loader.

 

export const action = () => {

  return json({errors: ["user", "email"]})

}

export default function GettingUsers() {

  const {errors} = useActionData() ?? {errors: []};

  return <div>

    {errors.map( e => <p>{e}</p>)}

  </div>

}

 

As mentioned at the beginning of this article, Remix is based on HTML forms for data flow. Accordingly, actions can be invoked using HTML forms. Remix provides its own form component, which is pre-rendered as a regular HTML form and replaced after hydration. In this way, the page remains usable even if it is not yet hydrated or if JavaScript is disabled in the browser.

The values to be passed are defined in <input> elements. If you need to define multiple actions in one segment, Remix has the concept of "Intents". Here, a hidden input field or a value defined in a button is used to define the name of the action, which is also passed when the form is submitted. On the server, the input field can be read and the corresponding logic can be executed. A login segment can look like the one shown in the code below. It defines two intents, login and logout. The action then executes the appropriate branch. The beauty of this model is, on the one hand, the separation of logic and visualization, and on the other hand, the possibility of automatic revalidation.

 

import { ActionFunction, LoaderFunction, json } from "@remix-run/node";

import { Form, useActionData, useLoaderData } from "@remix-run/react";

import {getSession, commitSession, destroySession} from "../sessions"

import { userService } from "~/userService.server";

 

export const loader: LoaderFunction = async({request}) => {

  const session = await getSession(request.headers.get("Cookie"))

  if(session.has("user")) {

    return json({user: session.get("user")})

  }

  return json({status: "OK"})

}

 

export const action: ActionFunction = async({request}) => {

  const form = await request.formData();

  const intent = form.get("intent")?.toString()

  if(intent === "login") {

    const username = form.get("username")

    const password = form.get("password")

    if(!username || !password) {

      return json({error: "Username and password is required"})

    }

    const isAuthenticated = userService.verifyAuth(username.toString(), password.toString())

    if(!isAuthenticated) {

      return json({error: "Username and password is wrong"})

    }

  

    const session = await getSession(request.headers.get("Cookie"))

    session.set("user", username)

    return json({status: "ok"}, {headers: {

      "Set-Cookie": await commitSession(session)

    }})

  } else if(intent === "logout") {

    return json({status: "ok"}, {headers: {"Set-Cookie": await destroySession(await getSession(request.headers.get("Cookie")))

    }})

  } else {

    return json({error: "Operation unknown"})

  }

}

export default function Index() {

  const {user} = useLoaderData<{user: string}>()

  const {error} = useActionData<{error: string}>() ?? {}

  return <div>

    { !!error && <p><b>{error}</b></p> }

    { user ? <p>Logged in as {user}</p> : <p>Not logged in.</p> }

    { user ? <LogoutForm/> : <LoginForm/>}

  </div>

}

function LoginForm() {

  return <Form method="post">

    <input type="username" required name="username" placeholder="Enter username"/>

    <input type="password" required name="password" placeholder="Enter Password" minLength={4}/>

    <button type="submit" name="intent" value="login">Login</button>

  </Form>

}

 

function LogoutForm() {

  return <Form method="post">

    <button type="submit" name="intent" value="logout">Logout</button>

  </Form>

}

 

After calling the action, Remix automatically starts the loaders of all active segments in the hydrated state. If the action results in changes to the site, Remix automatically ensures that they appear immediately.

Data flow and hydration

In the background, Remix creates for each action and loader an endpoint through which JSON data is called when the application is hydrated. This means that for new data, not the entire HTML document is passed, but only the relevant data in JSON format. If the application is not yet hydrated, the loader is called internally and the HTML document is returned with the modified state. For action, Remix waits for POST requests containing form data and responds with a modified HTML document.

Progressive Enhancement

Remix, using data flow, forces the development of a site so that it works well without client-side JavaScript, but can be enhanced with JavaScript. In addition to data flow, this concept can be found in routing, for example. Here, after hydration, instead of the whole HTML document, only new data in JSON format is requested, which is already preloaded before the page call thanks to the hover event.

This concept where JavaScript is not needed but improves the user experience is called Progressive Enhancement. Websites should utilize this concept wherever possible. There is nothing more confusing than a site that looks loaded but is not yet responsive to user actions. Progressive Enhancement should also be considered, especially with the growing trend of mobile device usage. This is because websites take longer to load scripts, especially on smartphones, due to poor wireless connectivity and poor performance.

Remix and Next.js

In order to compare Remix and Next.js, it is necessary to clarify which version or which strategy of Next.js is meant. This is because as of version 13, Next.js offers a new routing system that looks a lot like Remix routing. Prior to version 13, Next.js only offered "classic" routing. In Next.js, it was called "page routing." Pages could be stored in a pages folder, with each page defining an entire page. Next.js offers a similar page routing system as Remix. Instead of a loader, Next.js has functions getServerSideProps and getStaticProps. Both functions define data that can be read and used in rendering via a function parameter.

Next.js does not provide any actions for routing Next.js pages as Remix does. You can define API routes that can be called in a rendering function, for example with fetch. Thus, the data flow is less integrated by the framework. The developer has to take care of revalidation, which requires a separate API endpoint, because although Next.js automatically creates an API endpoint for getServerSideProps, the route URL changes with randomization in each build process.

As of version 13, there is new app routing. Instead of the pages folder, the app folder is used. The routing is based on nesting and, as in Remix, allows for advantages such as parallelization, "data proximity", less code duplication, etc. The new application routing also brings its concept of data with actions and revalidation, but has a different syntax and approach.

Conclusion

Remix is a new React framework that specializes in server-side rendering of websites. It tries to get as close as possible to the HTML5 API by building a data concept on top of the Forms API; this strategy is so old that it seems new. Despite the innovative concepts Remix offers, it is unlikely to prevail over Next.js as a framework for server-side rendering sites. Next.js is simply too common and established for that. There is already a large and stable ecosystem of tools, resources, libraries, and developers for Next.js. Remix can't compete with all of that.

So has Remix really failed? No, quite the opposite! Even if it doesn't become a mainstream framework, Remix has been able to illustrate the benefits of paradigms like Nested Routing or Progressive Enhancement. Thus, it is very likely that it indirectly contributed to the creation of a better routing concept for Next.js and possibly other frameworks.

Remix is constantly improving and will undoubtedly bring new innovations. Currently, for example, the Remix team is experimenting with connecting other javascript frameworks as well as the ability to create a project in pure javascript. Who knows, maybe this strategy will appear in other frameworks in the near future?

Popular Posts

My most popular posts

Maximum productivity on remote job
Business

Maximum productivity on remote job

I started my own business and intentionally did my best to work from anywhere in the world. Sometimes I sit with my office with a large 27-inch monitor in my apartment in Cheboksary. Sometimes I’m in the office or in some cafe in another city.

Hello! I am Sergey Emelyanov and I am hardworker
Business PHP

Hello! I am Sergey Emelyanov and I am hardworker

I am a programmer. I am an entrepreneur in my heart. I started making money from the age of 11, in the harsh 90s, handing over glassware to a local store and exchanging it for sweets. I earned so much that was enough for various snacks.

Hire Professional CRM developer for $25 per hour

I will make time for your project. Knowledge of Vtiger CRM, SuiteCRM, Laravel, and Vue.js. I offer cooperation options that will help you take advantage of external experience, optimize costs and reduce risks. Full transparency of all stages of work and accounting for time costs. Pay only development working hours after accepting the task. Accept PayPal and Payoneer payment systems. How to hire professional developer? Just fill in the form

Telegram
@sergeyem
Telephone
+4915211100235