Development

Blitz.js, ¿la evolución de Next?

Publicado por
David Tamayo
Blitz.js, ¿la evolución de Next?
Escrito por
David Tamayo
Publicado en
February 27, 2024
Tiempo de lectura
Categoría
Development

Blitzjs es un framework que cubre tanto el front como el back de una aplicación React.

Está basado en Next.js, pero incluye varias cosas más. Por ejemplo, el problema de la autenticación está resuelto con Blitz.

Login, logout, olvidé mi contraseña, inscripción de nuevos usuarios, manejo de sesiones… son funcionalidades que cuesta implementar de manera correcta, consumen tiempo y no son divertidas de hacer. Blitzjs se encarga de eso (nota: el emailer para reestablecer password no está incluido, pero blitz provee una base en mailers/forgotPasswordMailer.ts con la que podemos empezar).

Además, incluye una base de datos (que por defecto es sqlite, pero también soporta postgres) y un ORM para hablar con esta base de datos (prisma.io).

Incluye también un generador de código que se encarga de crear entidades, rutas, mutaciones y queries (OJO, conviene hacer commit antes de usar, porque reescribe código, aunque la herramienta igual advierte).

Se estima que en mayo o junio de 2021 saldrá la versión 1.0 de Blitzjs.

Para instalar la herramienta global y crear un proyecto debemos escribir:


yarn global add blitz

blitz new my-project

Si el intérprete de comandos no encuentra el comando blitz y estás en linux, usa:


~/.config/yarn/global/node_modules/blitz/bin/blitz new my-project

Otra solución sería crear un symlink mediante:


sudo ln -s ~/.config/yarn/global/node_modules/blitz/bin/blitz /usr/local/bin/blitz

El programa de setup nos da a elegir tres librerías para administrar formularios:

React Final Form (recomendado)

React Hook Form

Formik

En esta evaluación escogí React Final Form. Entramos al proyecto y lo levantamos con:


cd my-project

blitz dev

Otros comandos pueden instalar componentes como tailwind o material ui siguiendo recetas predefinidas:


blitz install tailwind

blitz install material-ui

La lista de recetas predefinidas no es muy larga, pero es de esperar que se vaya ampliando en el futuro.

Por el momento no hay recetas para instalar Ant Design, pero sí hay para Material UI.

La instalación de tailwind queda perfecta. Uno puede agregar:


className="w-full h-screen grid place-items-center"

a un div y el contenido se centra.

La base de datos vive en db/db.sqlite y pesa alrededor de 40 kilobytes tras el setup inicial (qué tierno). El esquema de esta base de datos está gestionada por el ORM primsa.io y vive en db/schema.prisma.

A su vez, este esquema está gestionado por el comando "blitz generate", que se encarga de crear entidades, rutas, mutaciones y queries.

Por ejemplo, cuando levantamos el proyecto este nos sugiere probar con el comando:


blitz generate all project name:string

Al ejecutarlo, blitz creará las siguientes rutas junto conlas mutaciones y queries para crear, borrar, editar y listar proyectos:


app/pages/projects/[projectId]/edit.tsx
app/pages/projects/[projectId].tsx
app/pages/projects/index.tsx
app/pages/projects/new.tsx
app/projects/components/ProjectForm.tsx
app/projects/queries/getProject.ts
app/projects/queries/getProjects.ts
app/projects/mutations/createProject.ts
app/projects/mutations/deleteProject.ts
app/projects/mutations/updateProject.ts

Si abrimos la base de datos con el programa sqlitebrowser podremos ver que hay dos tablas: User y Session.                                                              

Después del comando "blitz generate" tendremos una nueva tabla llamada Project, con las columnas id, createdAt, updatedAt y name.

Debemos notar que si usamos un query para obtener algún dato de manera asíncrona tendremos que usar unos tags <Suspense> para pintar un fallback, porque blitz usa react concurrent mode por defecto. (Cuando estamos en modo concurrent, a react no le gusta esperar sin tener algo que mostrar. El tag <Suspense> sirve para eso).

Entonces, cuando usemos un useQuery en un componente, tenemos que exportarlo envuelto con un <Suspense>. Por ejemplo, si tenemos una función Hello, la exportamos de la siguiente manera:


import { Suspense } from "react";
import { useQuery } from "blitz"

import getCurrentUser from "app/users/queries/getCurrentUser"



function Hello() {
  const [user] = useQuery(getCurrentUser, null)
  
  return (
    <div className="w-full h-screen grid place-items-center">
      hola {user?.name ?? ""}
     </div>
  );
}



export default function HelloPage() {
  return (
    <Suspense fallback={<div>Loading…</div>}>
      <About />
    </Suspense>
  );
}

Si esto resulta tedioso, se puede agregar el <Suspense> a nivel global, dentro de _app.tsx, sustituyendo donde dice:


{getLayout(<Component {...pageProps} />)}

por:


<React.Suspense fallback={<p>Loading…</p>}>
  {getLayout(<Component {...pageProps} />)}
</React.Suspense>

Aunque esto puede resultar demasiado genérico e incluso algo torpe.

Si quisiéramos restringir esta página para que la vean solo los usuarios logueados, podemos escribir:


HelloPage.authenticate = true

Una de las cosas divertidas de blitz es que el watcher también vigila la base de datos, que como se ha dicho es un archivo que pesa apenas 40 kilobytes. Si modifico la tabla User y actualizo la columna name y guardo los cambios en sqlitebrowser, la página /hello de inmediato muestra los cambios.

A modo de prueba, voy a modificar el formulario de inscripción para agregar el nombre del usuario. Actualmente, el formulario de inscripción pregunta un email y una password.

El formulario de inscripción vive en app/auth/components/SignupForm.tsx.

Voy a agregar un input para preguntar el nombre:


<LabeledTextField name="name" label="Name" placeholder="Name" type="text" />

Y voy a agregar un valor por defecto al formulario:


initialValues={{ email: "", password: "", name: "" }}

El formulario de inscripción queda así:



import { useMutation } from "blitz"
import { LabeledTextField } from "app/core/components/LabeledTextField"
import { Form, FORM_ERROR } from "app/core/components/Form"
import signup from "app/auth/mutations/signup"
import { Signup } from "app/auth/validations"

type SignupFormProps = {
  onSuccess?: () => void
}

export const SignupForm = (props: SignupFormProps) => {
  const [signupMutation] = useMutation(signup)

  return (
     <div className="w-full mt-12 grid place-items-center">
      <h1>Create an Account</h1>

      <Form
        submitText="Create Account"
        schema={Signup}
         initialValues={{ email: "", password: "", name: "" }}
        onSubmit={async (values) => {
          try {
            await signupMutation(values)
            props.onSuccess?.()
          } catch (error) {
            if (error.code === "P2002" && error.meta?.target?.includes("email")) {
              // This error comes from Prisma
              return { email: "This email is already being used" }
            } else {
              return { [FORM_ERROR]: error.toString() }
            }
          }
        }}
      >
        <LabeledTextField name="email" label="Email" placeholder="Email" />
        <LabeledTextField name="password" label="Password" type="password" />
         <LabeledTextField name="name" label="Name" placeholder="Name" type="text" />
      </Form>
    </div>
  )
}

export default SignupForm


Las validaciones de este formulario viven en: app/auth/validations.ts

Voy a agregar el name:


export const Signup = z.object({
  email: z.string().email(),
  password,
  name: z.string().min(3).max(100)
})

También hay que modificar la mutación responsable de inscribir nuevos usuarios, que vive en app/auth/mutations/signup.ts y que al final queda así:


import { resolver, SecurePassword } from "blitz"
import db from "db"
import { Signup } from "app/auth/validations"
import { Role } from "types"

export default resolver.pipe(resolver.zod(Signup), async (newUser, ctx) => {
  const { email, password, name } = newUser;
  const hashedPassword = await SecurePassword.hash(password.trim())
   
  const user = await db.user.create({
     data: {
       email: email.toLowerCase().trim(),
       name: name.trim(),
       hashedPassword,
       role: "USER"
     },
    select: { id: true, name: true, email: true, role: true },
  })
   
  await ctx.session.$create({ userId: user.id, role: user.role as Role })
  return user
})

Con esto guardaremos el nombre de los nuevos usuarios que se inscriban. Si luego vamos a la ruta /hello, los podremos saludar como corresponde.

Para construir el proyecto, hay que hacer “blitz build”, es muy similar a como se hace en next.js.

Conclusiones

Los inconvenientes que he encontrado al probar blitzjs son:

1. Uso de la tecnología experimental de react concurrent mode. Ant design es incompatible con ese modo de react y no es trivial instalarlo.

2. Conjunto limitado de recetas para instalar componentes.

El lado positivo es que por lo menos existen recetas para tailwind y material-ui. No más cruzar los dedos y perder el tiempo leyendo manuales para que la instalación de tailwind quede bien.

No más reinventar la rueda y perder el tiempo diseñando sistemas de autenticación a medio hacer. Blitz ofrece componentes de login a los que sólo falta agregar el logo corporativo y un par de retoques css para que se vea bien.

En resumen, blitzjs es muy similar a nextjs, salvo que añade autenticación, base de datos, recetas para instalar un número seleccionado de paquetes, ofreciendo de este modo una solución fullstack que bien podría reemplazar a nextjs.

Descarga nuestro Clever UI KIT 👇

Gracias. Te será enviado un mail confirmando la inscripción
¡Ups! Algo salió mal al enviar el formulario.
Gracias. Por rellenar el formulario
¡Ups! Algo salió mal al enviar el formulario.
Gracias. Te será enviado un mail confirmando la inscripción
¡Ups! Algo salió mal al enviar el formulario.
Gracias. Te será enviado un mail confirmando la inscripción
¡Ups! Algo salió mal al enviar el formulario.
Gracias. Por rellenar el formulario
¡Ups! Algo salió mal al enviar el formulario.
Gracias. Por rellenar el formulario
¡Ups! Algo salió mal al enviar el formulario.

Crea tu propio manual de marca con esta plantilla gratuita.
¡Organiza tus activos de diseño de forma más eficiente!