¿Cómo alojar una aplicación Remix?
En este artículo, aprenderá todo lo que necesita saber para comenzar con Remix. Analizaremos sus ventajas y desventajas, demostraremos cómo crear una aplicación Remix y, por último, cómo implementar Remix.
Contents
¿Qué es Remix?
Remix es un marco web moderno y completo para crear aplicaciones rápidas, ingeniosas y resistentes. Aprovecha el poder de la representación del lado del servidor (SSR), lo que permite tiempos de carga más rápidos y un SEO mejorado para aplicaciones web dinámicas.
Con Remix, los desarrolladores pueden integrar perfectamente la lógica del lado del cliente y del servidor dentro de una base de código unificada. En comparación con otros marcos populares de React, Remix hace muchas cosas de manera diferente. Eso incluye:
- Enrutamiento basado en archivos (un archivo en el directorio de rutas representa una ruta específica)
- Manejo de formularios tradicional (usando solicitudes HTML y HTTP, similar a PHP)
- Gestión de estado (el estado solo se almacena en el servidor)
- Carga de datos (desacoplada de los componentes)
Remix se creó en 2020 e inicialmente estuvo disponible bajo una tarifa de licencia anual. Posteriormente, en octubre de 2021, el equipo de Remix decidió hacer que el proyecto sea de código abierto. Ahora está disponible bajo la licencia MIT.
A finales de 2022, Shopify adquirió Remix por 2.100 millones de dólares.
Beneficios de Remix
Let’s look at the main benefits of using the Remix framework.
Veamos los principales beneficios de utilizar el marco Remix.
Remix está construido sobre React Router, una poderosa solución de enrutamiento del lado del cliente. De hecho, Remix fue creado por el mismo equipo de desarrolladores que creó React Router.
El marco utiliza un sistema de navegación basado en archivos, que simplifica la organización del código. Permite a los desarrolladores asociar rutas, componentes y recursos con archivos o directorios específicos.
Aquí hay un ejemplo:
app/
└── routes/
├── $noteId.tsx // matches: /<noteId>/
├── $noteId_.destroy.tsx // matches: /<noteId>/destroy
├── $noteId_.edit.tsx // matches: /<noteId>/edit
├── _index.tsx // matches: /
└── create.tsx // matches: /create
Lo mejor de todo es que admite enrutamiento anidado a través de <Outlet/>. ¡Esto da como resultado tiempos de carga más cortos, un mejor manejo de errores y más!
Server-Side Rendering (SSR)
Remix aprovecha el poder del renderizado del lado del servidor.
En las aplicaciones Vanilla React, los datos generalmente se obtienen en el lado del cliente y luego se inyectan en el DOM. Esto se conoce como renderizado del lado del cliente (CSR, “client-side rendering”).
Remix, por otro lado, adopta un enfoque diferente. Primero obtiene los datos en el backend, representa el HTML con los datos obtenidos y luego los entrega al cliente.
La renderización del lado del servidor tiende a dar como resultado un mejor rendimiento y aplicaciones más compatibles con SEO.
Manejo de formularios
Remix toma forma de volver a lo básico.
En lugar de utilizar un montón de componentes controlados y JavaScript, utiliza formularios HTML tradicionales y solicitudes HTTP.
Cuando se envía un formulario, envía una solicitud HTTP a una ruta específica, que luego se procesa en el lado del servidor utilizando la función action(). Funciona de manera similar al viejo PHP.
Esto significa que Remix no requiere absolutamente ningún JavaScript para procesar formularios. Si bien esto es excelente, puede hacer que la validación del formulario y la visualización de errores sean un poco más complicadas.
Administración de estados
En este contexto, el estado se refiere a la sincronización de los datos del servidor y del cliente.
Remix hace que la gestión estatal sea muy sencilla. Elimina la necesidad de Redux, Zustand, React Query, Apollo o cualquier otra biblioteca de gestión de estado del lado del cliente.
Cuando se utiliza Remix, todo el estado lo maneja el servidor. El cliente prácticamente no posee ningún estado; por lo tanto, no se requiere el proceso de sincronización.
Los datos del servidor al cliente se pasan a través de varias funciones como loader() y action().
Transiciones y UI optimista
Las transiciones Remix hacen que el movimiento entre páginas sea fluido y rápido al cargar datos por adelantado y usar animaciones.
La interfaz de usuario optimista refleja instantáneamente las acciones del usuario, lo que hace que los sitios web se sientan más receptivos al mostrar los cambios antes de que se confirmen.
Las transiciones y la interfaz de usuario optimista mejoran enormemente la experiencia del usuario al reducir la latencia percibida y proporcionar retroalimentación inmediata.
Limitaciones de Remix
Aunque Remix es genial, tiene algunas limitaciones.
Complejo
Remix no es el marco más fácil de usar y su documentación (al momento de escribir este artículo) no es la mejor.
El marco también hace muchas cosas diferentes a Vanilla React. Si eres desarrollador de React, puede que te lleve algo de tiempo entender todos los diferentes conceptos de Remix.
Menos popular
Remix es todavía un marco relativamente nuevo. Su lanzamiento público solo se remonta a octubre de 2021. Es menos maduro y está menos probado en comparación con algunos competidores, como Next.js.
Al observar las estrellas de GitHub, podemos ver que Remix (27k estrellas) es mucho menos popular que Next.js (120k estrellas). Al momento de escribir este artículo, Remix no ha sido adoptado por ningún gigante tecnológico excepto Shopify.
Estrechamente acoplado
Las aplicaciones Remix tienen un frontend y un backend estrechamente acoplados. Si bien este enfoque funciona para proyectos más pequeños, algunos desarrolladores prefieren la flexibilidad de un frontend y un backend separados.
Además, separar el frontend del backend puede hacer que su código sea más fácil de mantener y probar.
Sin SSG ni ISR
Remix no admite la generación de sitios estáticos (SSG, o “static site generation”) ni la regeneración estática incremental (ISR, o “incremental static regeneration”).
¿Cómo implementar una aplicación Remix?
En esta sección, crearemos e implementaremos una aplicación Remix.
Requisitos previos
Para seguir este proceso, necesitará:
- Comprensión básica de TypeScript
- Experiencia con Docker (y tecnología de contenedorización)
- Node.js y un IDE de JavaScript instalado en su máquina
- Docker Desktop instalado en su máquina
Descripción del proyecto
A lo largo del artículo, trabajaremos en una aplicación web de notas. La aplicación web permitirá a los usuarios crear, recuperar, editar y eliminar notas.
Para el backend, utilizaremos Back4app BaaS y para el frontend, usaremos el marco Remix. Una vez que la interfaz esté codificada, la implementaremos en Back4app Containers.
El producto final se verá así:
Le sugiero que primero siga la aplicación web de notas. Después del artículo, debería poder implementar sus propias aplicaciones Remix.
Backend
En esta sección de artículo, usaremos Back4app para construir el backend de nuestra aplicación.
Crear aplicación
Comience iniciando sesión en su cuenta de Back4app (o creando una si aún necesita obtenerla).
Al iniciar sesión, será redirigido al portal “Mis aplicaciones”. Para crear un backend, primero necesita crear una aplicación Back4app. Para hacer eso, haga clic en “Crear nueva aplicación”.
Back4app ofrece dos soluciones:
- Backend como servicio (BaaS): una solución de backend completa
- Contenedores como servicio (CaaS): plataforma de orquestación de contenedores basada en Docker
Como estamos trabajando en un backend, seleccione “Backend como servicio”.
Asigne un nombre a su aplicación, deje todo lo demás como predeterminado y haga clic en “Crear”.
La plataforma tardará un poco en configurar todo lo necesario para su backend. Eso incluye la base de datos, la interfaz de la aplicación, el escalado, la seguridad, etc.
Una vez que su aplicación esté lista, será redirigido a la vista de la base de datos en tiempo real de la aplicación.
Base de datos
Continuando, cuidemos la base de datos.
Dado que estamos creando una aplicación relativamente simple, solo necesitaremos un modelo: llamémoslo Note. Para crearlo, haga clic en el botón “Crear una clase” en la parte superior izquierda de la pantalla.
Asígnele el nombre Note, habilite “Lectura y escritura pública habilitadas” y haga clic en “Crear clase y agregar columnas”.
Luego, agregue las siguientes columnas:
+-----------+--------------+----------------+----------+
| Data type | Name | Default value | Required |
+-----------+--------------+----------------+----------+
| String | emoji | <leave blank> | yes |
+-----------+--------------+----------------+----------+
| String | title | <leave blank> | yes |
+-----------+--------------+----------------+----------+
| File | content | <leave blank> | no |
+-----------+--------------+----------------+----------+
Una vez que haya creado la clase, complete la base de datos con datos de muestra. Crea algunas notas proporcionando los emojis, títulos y contenido. Alternativamente, importe esta: exportación de base de datos.
¡Genial, eso es todo!
Hemos creado con éxito un backend sin escribir ningún código.
Para obtener más información sobre Backend como Servicio, consulte ¿Qué es el Backend como Servicio?
Frontend
En esta sección de artículo, construiremos la interfaz de nuestra aplicación utilizando el marco Remix.
create-remix
La forma más sencilla de iniciar un proyecto Remix es mediante la utilidad create-remix. Esta herramienta crea un proyecto Remix listo para producción.
Se encarga de la estructura del directorio y las dependencias, configura el paquete, etc.
Cree un nuevo proyecto Remix ejecutando el siguiente comando:
$ npx create-remix@latest remix-notes
Initialize a new git repository? Yes
Install dependencies with npm? Yes
Si nunca ha usado create-remix, se instalará automáticamente.
Una vez que se haya creado el proyecto, cambie su directorio activo a él:
$ cd remix-notes
Por último, inicie el servidor de desarrollo:
$ npm run dev
Abra su navegador web favorito y navegue hasta http://localhost:5173. La página de inicio predeterminada de Remix debería aparecer en su pantalla.
TailwindCSS
Para hacernos la vida un poco más fácil, usaremos TailwindCSS. TailwindCSS es un marco de utilidades que le permite crear interfaces rápidamente sin escribir ningún CSS simple.
Primero, instálelo usando npm:
$ npm install -D tailwindcss postcss autoprefixer
A continuación, ejecute tailwindcss init:
$ npx tailwindcss init --ts -p
Esto configurará su proyecto y creará un archivo tailwind.config.ts en la raíz del proyecto.
Modifique su propiedad content de esta manera para que Tailwind sepa en qué archivos se utilizarán las clases de utilidad:
// tailwind.config.ts
import type {Config} from "tailwindcss";
export default {
content: ["./app/**/*.{js,jsx,ts,tsx}"], // new
theme: {
extend: {},
},
plugins: [],
} satisfies Config;
Cree un nuevo archivo llamado tailwind.css en el directorio de la aplicación con el siguiente contenido:
/* app/tailwind.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Por último, impórtelo en root.tsx mediante links:
// app/root.tsx
// other imports
import {LinksFunction} from "@remix-run/node";
import stylesheet from "~/tailwind.css?url";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet },
];
// ...
¡Eso es todo!
Hemos instalado TailwindCSS con éxito.
Rutas
Nuestra aplicación web tendrá los siguientes puntos finales:
/
muestra todas las notas/permite a los usuarios crear notas
/<noteId>
muestra una nota específica/<noteId>/permite a los usuarios eliminar una nota específica
Para definir estas rutas, cree la siguiente estructura de directorios en la carpeta de la aplicación:
app/
└── routes/
├── $noteId.tsx
├── $noteId_.destroy.tsx
├── $noteId_.edit.tsx
├── _index.tsx
└── create.tsx
Como habrá adivinado, el prefijo $ se usa para parámetros dinámicos y . se utiliza en lugar de /.
Vistas
Continuando, implementemos las vistas.
Para que nuestro código sea más seguro, crearemos una interfaz llamada Note que se parece a nuestra clase de base de datos Note.
Cree una carpeta llamada store y dentro de ella, cree NoteModel.ts con el siguiente contenido:
// app/store/NoteModel.ts
export default interface NoteModel {
objectId: string;
emoji: string;
title: string;
content: string;
createdAt: Date;
updatedAt: Date;
}
Luego pegue el código de vista para _index.tsx, $noteId.tsx y create.tsx:
// app/routes/_index.tsx
import {Link, NavLink} from "@remix-run/react";
import NoteModel from "~/store/NoteModel";
const notes = [
{objectId: "1", emoji: "📝", title: "My First Note"},
{objectId: "2", emoji: "📓", title: "My Second Note"},
{objectId: "3", emoji: "📔", title: "My Third Note"},
] as NoteModel[];
export default function Index() {
return (
<>
<Link to={`/create`}>
<div className="bg-blue-500 hover:bg-blue-600 text-lg font-semibold text-white
px-4 py-3 mb-2 border-2 border-blue-600 rounded-md"
>
+ Create
</div>
</Link>
{notes.map(note => (
<NavLink key={note.objectId} to={`/${note.objectId}`}>
<div className="hover:bg-slate-200 text-lg font-semibold
px-4 py-3 mb-2 border-2 border-slate-300 rounded-md"
>
{note.emoji} {note.title}
</div>
</NavLink>
))}
</>
);
}
// app/routes/$noteId.tsx
import {Form} from "@remix-run/react";
import NoteModel from "~/store/NoteModel";
const note = {
objectId: "1", emoji: "📝", title: "My First Note", content: "Content here.",
createdAt: new Date(), updatedAt: new Date(),
} as NoteModel;
export default function NoteDetails() {
return (
<>
<div className="mb-4">
<p className="text-6xl">{note.emoji}</p>
</div>
<div className="mb-4">
<h2 className="font-semibold text-2xl">{note.title}</h2>
<p>{note.content}</p>
</div>
<div className="space-x-2">
<Form
method="post" action="destroy"
onSubmit={(event) => event.preventDefault()}
className="inline-block"
>
<button
type="submit"
className="bg-red-500 hover:bg-red-600 font-semibold text-white
p-2 border-2 border-red-600 rounded-md"
>
Delete
</button>
</Form>
</div>
</>
);
}
// app/routes/create.tsx
import {Form} from "@remix-run/react";
export default function NoteCreate() {
return (
<>
<div className="mb-4">
<h2 className="font-semibold text-2xl">Create Note</h2>
</div>
<Form method="post" className="space-y-4">
<div>
<label htmlFor="emoji" className="block">Emoji</label>
<input
type="text" id="emoji" name="emoji"
className="w-full border-2 border-slate-300 p-2 rounded"
/>
</div>
<div>
<label htmlFor="title" className="block">Title</label>
<input
type="text" id="title" name="title"
className="w-full border-2 border-slate-300 p-2 rounded"
/>
</div>
<div>
<label htmlFor="content" className="block">Content</label>
<textarea
id="content" name="content"
className="w-full border-2 border-slate-300 p-2 rounded"
/>
</div>
<div>
<button
type="submit"
className="bg-blue-500 hover:bg-blue-600 font-semibold
text-white p-2 border-2 border-blue-600 rounded-md"
>
Create
</button>
</div>
</Form>
</>
);
}
No hay nada sofisticado en este código. Todo lo que hicimos fue usar JSX en combinación con TailwindCSS para crear la interfaz de usuario.
Como habrá notado, todos los componentes no están controlados (no usamos useState()). Además de eso, estamos usando un formulario HTML real.
Esto se debe a que el marco Remix maneja formularios similares a PHP usando solicitudes HTTP, a diferencia de React.
Parse
Hay varias formas de conectarse a su backend basado en Back4app. Puede usar:
- RESTful API
- GraphQL API
- Parse SDK
La forma más fácil y robusta es sin duda el SDK de Parse. El SDK de Parse es un kit de desarrollo de software que proporciona una serie de clases de utilidades y métodos para consultar y manipular fácilmente sus datos.
Comience instalando Parse a través de npm:
$ npm install -i parse @types/parse
Cree un archivo .env en la raíz del proyecto así:
# .env
PARSE_APPLICATION_ID=<your_parse_app_id>
PARSE_JAVASCRIPT_KEY=<your_parse_js_key>
Asegúrese de reemplazar <your_parse_app_id> y <your_parse_js_key> con credenciales reales. Para obtener las credenciales, navegue hasta su aplicación y seleccione “Configuración de la aplicación> Servidor y seguridad” en la barra lateral.
Luego, inicialice Parse en la parte superior del archivo root.tsx de esta manera:
// app/root.tsx
// other imports
import Parse from "parse/node";
const PARSE_APPLICATION_ID = process.env.PARSE_APPLICATION_ID || "";
const PARSE_HOST_URL = "https://parseapi.back4app.com/";
const PARSE_JAVASCRIPT_KEY = process.env.PARSE_JAVASCRIPT_KEY || "";
Parse.initialize(PARSE_APPLICATION_ID, PARSE_JAVASCRIPT_KEY);
Parse.serverURL = PARSE_HOST_URL;
Crearemos un archivo separado llamado api/backend.ts para mantener nuestras vistas limpias de la lógica de comunicación del backend.
Cree api/backend.ts con el siguiente contenido:
// app/api/backend.ts
import Parse from "parse/node";
import NoteModel from "~/store/NoteModel";
export const serializeNote = (note: Parse.Object<Parse.Attributes>): NoteModel => {
return {
objectId: note.id,
emoji: note.get("emoji"),
title: note.get("title"),
content: note.get("content"),
createdAt: new Date(note.get("createdAt")),
updatedAt: new Date(note.get("updatedAt")),
};
}
export const getNotes = async (): Promise<NoteModel[]> => {
// ...
}
export const getNote = async (objectId: string): Promise<NoteModel | null> => {
// ...
}
// Grab the entire file from:
// https://github.com/duplxey/back4app-containers-remix/blob/master/app/api/backend.ts
Por último, modifique las vistas para recuperar y manipular los datos del backend:
// app/routes/index.tsx
import {json} from "@remix-run/node";
import {Link, NavLink, useLoaderData} from "@remix-run/react";
import {getNotes} from "~/api/backend";
export const loader = async () => {
const notes = await getNotes();
return json({notes});
};
export default function Index() {
const {notes} = useLoaderData<typeof loader>();
return (
// ...
);
}
// app/routes/$noteId.tsx
import {getNote} from "~/api/backend";
import {json, LoaderFunctionArgs} from "@remix-run/node";
import {Form, useLoaderData} from "@remix-run/react";
import {invariant} from "@remix-run/router/history";
export const loader = async ({params}: LoaderFunctionArgs) => {
invariant(params.noteId, "Missing noteId param");
const note = await getNote(params.noteId);
if (note == null) throw new Response("Not Found", {status: 404});
return json({note});
};
export default function NoteDetails() {
const {note} = useLoaderData<typeof loader>();
return (
// ...
);
}
// app/routes/create.tsx
import {ActionFunctionArgs, redirect} from "@remix-run/node";
import {Form} from "@remix-run/react";
import {createNote} from "~/api/backend";
export const action = async ({request}: ActionFunctionArgs) => {
const formData = await request.formData();
const {emoji, title, content} = Object.fromEntries(formData)
as Record<string, string>;
const note = await createNote(emoji, title, content);
return redirect(`/${note?.objectId}`);
};
export default function NoteCreate() {
return (
// ...
);
}
// app/routes/$noteId_.destroy.tsx
import type {ActionFunctionArgs} from "@remix-run/node";
import {redirect} from "@remix-run/node";
import {invariant} from "@remix-run/router/history";
import {deleteNote} from "~/api/backend";
export const action = async ({params}: ActionFunctionArgs) => {
invariant(params.noteId, "Missing noteId param");
await deleteNote(params.noteId);
return redirect(`/`);
};
Resumen de código:
- Usamos la función Remix loader() para cargar los datos. Una vez cargados los datos, los pasamos a la vista como JSON mediante la función json().
- Usamos la función Remix action() para manejar el envío del formulario (por ejemplo, POST).
- El noteId se pasó a las vistas como parámetro.
La aplicación ahora debería estar completamente funcionando y sincronizada con el backend de Back4app. Asegúrese de que todo funcione creando algunas notas, editándolas y luego eliminándolas.
Dockerización de la aplicación
En esta sección, acoplaremos nuestra interfaz Remix.
Dockerfile
Un Dockerfile es un archivo de texto sin formato que describe los pasos que debe realizar el motor Docker para construir una imagen.
Estos pasos abarcan configurar el directorio de trabajo, especificar la imagen base, transferir archivos, ejecutar comandos y más.
Las instrucciones generalmente se representan en mayúsculas e inmediatamente seguidas de sus respectivos argumentos.
Para obtener más información sobre todas las instrucciones, consulte la referencia de Dockerfile.
Cree un Dockerfile en la raíz del proyecto con el siguiente contenido:
# Dockerfile
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start"]
Este Dockerfile se basa en la imagen del node:20. Primero configure el directorio de trabajo, copie elpackage.json e instale las dependencias.
Después de eso, construya el proyecto, exponga el puerto 3000 y entregue la aplicación.
.dockerignore
Cuando trabaje con Docker, normalmente se esforzará por crear imágenes que sean lo más pequeñas posible.
Dado que nuestro proyecto contiene ciertos archivos que no necesitan estar en la imagen (por ejemplo, .git, build, configuración IDE), los excluiremos. Para hacer eso, crearemos un archivo .dockerignore que funciona de manera similar a un archivo .gitignore.
Cree un archivo .dockerignore en la raíz del proyecto:
# .dockerignore
.idea/
.cache/
build/
node_modules/
Adapte el .dockerignore según las necesidades de su proyecto.
Compile y pruebe
Antes de enviar una imagen de Docker a la nube, es una buena idea probarla localmente.
Primero, compile la imagen:
$ docker build -t remix-notes:1.0 .
A continuación, cree un contenedor utilizando la imagen recién creada:
$ docker run -it -p 3000:3000
-e PARSE_APPLICATION_ID=<your_parse_app_id>
-e PARSE_JAVASCRIPT_KEY=<your_parse_javascript_key>
-d remix-notes:1.0
Asegúrese de reemplazar <your_parse_app_id> y <your_parse_javascript_key> con las credenciales reales.
Ahora debería poder accederse a su aplicación en http://localhost:3000. Debería comportarse de la misma manera que antes del proceso de dockerización.
Subir a GitHub
Para implementar una aplicación en Back4app Containers, primero debe enviar su código fuente a GitHub. Para hacer eso, puedes seguir estos pasos:
- Inicie sesión en su cuenta de GitHub (o regístrese).
- Cree un nuevo repositorio de GitHub.
- Navegue hasta su proyecto local e inicialícelo: git init
- Agregue todo el código al sistema de control de versiones: git add.
- Agregue el origen remoto a través de git remote add origin <remote_url>
- Confirme todo el código mediante git commit -m “initial commit”
- Envíe el código a GitHub git push origin master
Implementar aplicación
En esta última sección, implementaremos la interfaz en Back4app Containers.Inicie sesión en su cuenta Back4app y haga clic en “Crear nueva aplicación” para inicializar el proceso de creación de la aplicación.
Dado que ahora estamos implementando una aplicación en contenedores, seleccione “Contenedores como servicio”.
A continuación, deberá vincular su cuenta de GitHub con Back4app e importar el repositorio que ha creado previamente. Una vez conectado, seleccione el repositorio.
Back4app Containers permite una configuración avanzada. Sin embargo, para nuestra sencilla aplicación, las siguientes configuraciones serán suficientes:
- Nombre de la aplicación: remix-notes (o elija su nombre)
- Variables de entorno: PARSE_APPLICATION_ID, PARSE_JAVASCRIPT_KEY
Utilice los valores que utilizó en el archivo .env para las variables de entorno.
Una vez que haya terminado de configurar la implementación, haga clic en “Implementar”.
Espere unos momentos hasta que se complete la implementación. Una vez implementada, haga clic en el enlace verde en el lado izquierdo de la pantalla para abrir la aplicación en su navegador.
¡Eso es todo! Su aplicación ahora se implementó correctamente y se puede acceder a ella a través del enlace proporcionado. Además, Back4app ha emitido un certificado SSL gratuito para su aplicación.
Conclusión
A pesar de ser un marco relativamente nuevo, Remix permite a los desarrolladores crear potentes aplicaciones web completas.
El marco aborda muchas complejidades de las aplicaciones web, como el manejo de formularios, la gestión del estado y más.
En este artículo, ha aprendido cómo crear e implementar una aplicación Remix. Ahora debería poder utilizar Back4app para crear un backend simple y Back4app Containers para implementar sus aplicaciones en contenedores.
El código fuente del proyecto está disponible en el repositorio back4app-containers-remix.