Guardando los datos
Añadamos una nueva tabla. Abra api/db/schema.prisma
y añada un modelo de "Contact" luego del modelo Post existente:
Sintaxis de Prisma para campos opcionales
Indique un campo como opcional (que acepte
NULL
como valor) con un signo de interrogación luego del tipo de datos, ej.String?
. Esto hace quename
sea de tipoString
oNULL
.
Luego creamos y aplicamos una migración:
Podemos nombrarla "crear contactos".
Ahora creemos la interfaz GraphQL para acceder a la tabla. Aún no hemos usado este comando generate
(si bien el comando scaffold
se usa detrás de escena):
Al igual que el comando scaffold
, se crean dos nuevos archivos en el directorio api
:
api/src/graphql/contacts.sdl.js
: contiene el esquema de GraphQLapi/src/services/contacts/contacts.js
: contiene la lógica de negocios.
A continuación abra api/src/graphql/contacts.sdl.js
, verá ya definidos los tipos Contact
, CreateContactInput
y UpdateContactInput
: el comando generate sdl
ha escrito el esquema y ha creado un tipo Contact
con cada campo de la tabla, así como un tipo Query
con la consulta contacts
que devuelve una list de Contact
:
¿Qué son CreateContactInput
y UpdateContactInput
? Redwood sigue la recomendación GraphQL de usar tipos de entrada en mutaciones en lugar de listar todos los campos explícitamente. Cada campo requerido en schema.prisma
será obligatorio en CreateContactInput
(impide crear registros inválidos) pero no así en UpdateContactInput
. Esto se debe a que puede actualizar uno o más campos. La alternativa sería separar los tipos de entrada para cada permutación de campos a actualizar. Nos parece que es mejor tener una sola entrada aunque no sea la forma perfecta de hacer una API GraphQL.
Redwood asume que el código no establece un valor para campos
id
ocreatedAt
por lo los deja fuera de los tipos de entrada, pero si la base de datos requiere actualizaciones manuales Ud puede agregarlos tanto aCreateContactInput
como aUpdateContactInput
.
Dado que todas las columnas en el archivo schema.prisma
son requeridas, se marcan con el signo de exclamación !
al final del tipo de datos (ej. nombre: String!
). ).
Sintaxis GraphQL para campos requeridos
La syntaxis de GraphQL's require un símbolo de exclamación
!
para indicar que el campo es requerido. Recuerde también:schema.prisma
requiere un símbolo de pregunta?
cuando un campo es opcional.
Como se describe en Misión secundaria: Cómo funciona Redwood con datos, no hay resolutores explícitos definidos en el archivo SDL. Redwood usa una nomenclatura simple: cada campo listado en los tipos Query
y Mutation
en el archivo sdl
(ej api/src/graphql/contacts.sdl.js
) mapea una función con el mismo nombre en el archivo services
(api/src/services/contacts/contacts.js
).
En este caso creamos una única Mutation
que llamaremos createContact
. Agregue esto al final del archivo SDL (antes de cerrar el tilde):
La mutación createContact
acepta una variable input
, que es un objeto que se ajusta a la definición de CreateContactInput
, es decir, { name, email, message }
.
Terminado el SDL, definamos el servicio que guardará los datos en la base de datos. El servicio incluye una función predeterminada de contacts
para obtener todos los contactos de la base de datos. Añadamos nuestra mutación para crear un nuevo contacto:
¡Gracias a Prisma se necesita poco código para guardar datos! Esta es una llamada asíncrona pero no tuvimos que preocuparnos por resolver Promesas o tratar con async/await
. ¡Apollo lo hizo por nosotros!
Antes de conectarlo a la interfaz de usuario echemos un vistazo a la GUI ejecutando yarn redwood dev
.
#
GraphQL PlaygroundA menudo es útil experimentar y llamar a su API en una forma más "cruda" antes de avanzar en la implementación y ver que falta algo. Por ejemplo, ¿qué pasa si hubiera un error tipográfico en la API o en la capa web? Descubrámoslo accediendo a la API.
Al iniciar el desarrollo con yarn redwood dev
se inician dos procesos al mismo tiempo. Abra una nueva pestaña del navegador y vaya a http://localhost:8911/graphql Este es el Playground GraphQL de Apollo, un GUI web para las APIs GraphQL:
Aún no es muy emocionante, pero revise la pestaña "Docs" en la extrema derecha:
¡Es el esquema completo tal como lo definen los archivos SDL! El Playground usa estas definiciones y le dará sugerencias para ayudarle a construir consultas en la parte izquierda. Pruebe obtener los IDs de todos los posts de la base de datos; escribe la consulta a la izquierda y haz clic en el botón "Reproducir" para ejecutarla:
El Playground GraphQL es una excelente manera de experimentar con la API o resolver problemas cuando una consulta o mutación que no se comporte de forma esperada.
#
Creando un contactoLa mutación GraphQL está lista para en el backend así que sólo queda es invocarla desde el frontend. Como nuestro formulario está en la página ContactPage
tiene sentido invocar ahí la mutación. Primero definamos la mutación como una constante (que usaremos más tarde) fuera del componente después de las declaraciones de import
:
La mutación createContact
que definimos en el SDL de Contactos recibe un objeto de entrada
que contiene los campos: nombre, correo electrónico y mensaje.
A continuación llamaremos al hook (gancho) useMutation
proporcionado por Apollo que ejecutará la mutación cuando estemos listos (recuerde el import
):
create
es una función que invoca la mutación y recibe un objeto cuya clave variables
contiene otro objeto con una clave input
. Por ejemplo:
Si recuerda la <Form>
nos da todos los campos en un objeto donde la clave es el nombre del campo, por lo que el objeto data
que recibimos en onSubmit
ya tiene el formato que necesitamos para la variable input
!
Actualicemos la función onSubmit
para invocar la mutación con los datos que recibe:
Intente completar y enviar el formulario —¡debería tener un nuevo contacto en la base de datos! Puedes verificarlo que con el Playground GraphQL:
#
Mejorar el formulario de contactoNuestro formulario funciona pero tiene algunos problemas:
- Al hacer clic varias veces en el botón de enviar se realizarán varios envíos
- El usuario no sabe si el envío fue exitoso
- Si ocurriera un error en el servidor, no tenemos forma de notificar al usuario
Abordemos estos problemas.
El hook useMutation
devuelve un par de elementos más junto con la función para invocarlo. Podemos deconstruir el segundo elemento de la matriz devuelta. Los dos que nos preocupan son loading
y error
:
Sabremos si la llamada a la base de datos sigue cargando si miramos loading
. Una solución fácil para nuestro problema de envío múltiple sería desactivar el botón de enviar si la llamada todavía está cargando. Podemos fijar el atributo desactivado
al botón "Guardar" según el valor de loading
:
Es difícil ver diferencias en el entorno de desarrollo pues el envío es muy rápido. Sin embargo, puede simular una conexión lenta a través de la pestaña de Red del Inspector Web de Chrome:
Verá que el botón "Guardar" se desactiva durante unos instantes mientras esperas la respuesta.
A continuación, notificaremos al usuario de que su envío fue exitoso. Redwood incluye la biblioteca react-hot-toast para mostrar notificaciones en la página.
useMutation
acepta como un segundo argumento un un objeto de opciones. Entre ellas una función de callback, onCompleted
, que se invoca al completar la mutación con éxito. Usaremos ese callback para invocar la función toast()
que añadirá un mensaje para mostrar en un componente <Toaster>.
Añada el callback onCompleted
a useMutation
e incluya el componente <Toaster> en el bloque de return
, justo antes del <Form>. Envuelva todo en un fragmento (<></>) para devolver un solo elemento:
Puede leer la documentación completa de Toast aquí.
#
Mostrando errores del servidorA continuación informaremos al usuario de errores en el servidor. Hasta ahora solo hemos notificado errores del lado del cliente: un campo faltante o formato erróneo. Pero si tenemos controles del lado del servidor que <Form>
no conozca, necesitaremos que el usuario sepa que falla.
Tenemos una validación para correo en el cliente, pero todo buen desarrollador sabe que nunca debe confía en el cliente. Añadamos la validación de correo en la API para estar seguros de los datos son válidos en nuestra base de datos, incluso cuando alguien pase por alto la validación del lado del cliente.
¿No hay validación en el servidor?
¿Por qué no validamos en el servidor la existencia de nombre, correo y mensaje? Porque la base de datos lo hace por nosotros. ¿Recuerdas el tipo
String!
en la definición SDL? Esto añade una restricción en la base de datos: el campo no puede sernull
. Si unnull
llegara hasta la base de datos, se rechazaría el comando insert/update y GraphQL devolvería un error al cliente.¡Como no hay un tipo
Hemos hablado de lógica de negocios perteneciente a los servicios y este es un ejemplo perfecto. Añadamos una función validate
al servicio contacts
:
Así que cuando createContact
se llame primero validará los inputs y sólo creará el registro en la base de datos cuando no hayan errores.
Ya capturamos cualquier error existente en la constante error
que obtuvimos de useMutation
, así que podríamos mostrar un cuadro de error en la página por ejemplo al principio del formulario:
Para manejar errores manualmente, puede hacer lo siguiente:
Para ver el error en el servidor, eliminamos la validación del formato de correo del lado del cliente:
Intente rellenar el formulario con una dirección de correo no válida:
No es bonito, pero funciona. Estaría bueno si el campo en sí mismo fuera resaltado como cuando la validación estaba anteriormente...
¿Recuerda cuando le dijimos que <Form>
tenía otro truco en la manga? ¡Aquí viene!
Quite la pantalla de errores que acabamos de añadir ({ error && ...
) y reemplácelo por <FormError>
, pasandole la constante error
que obtuvimos de useMutation
y apliquemos un estilo a wrapperStyle
(no olvide el import
). También pasamos el error
a <Form>
para que pueda configurar el contexto:
Ahora envía un mensaje con una dirección de correo inválida:
Obtenemos ese mensaje de error en la parte superior diciendo que algo salió mal en inglés y el campo real se resalta, ¡al igual que la validación en línea! El mensaje en la parte superior puede ser demasiado para un formulario tan breve, pero puede ser clave si el formulario comprende varias pantallas; el usuario tendrá un resumen de lo que salió mal en un solo lugar y sin recurrir a buscar por todos lados los campos en rojo. No tiene porque usar este cuadro de mensajes, bastará eliminar el <FormError>
y cada campo será resaltado como se espera.
Opciones de estilo para
<FormError>
<FormError>
tiene varias opciones de estilo asociadas a varias partes del mensaje:
wrapperStyle
/wrapperClassName
: para el contenedor del mensajetitleStyle
/titleClassName
: para el título "Can't create new contact"listStyle
/listClassName
: el elemento<ul>
con la lista de erroreslistItemStyle
/listItemClassName
: cada elemento<li>
para cada error
#
Una cosa más...Dado que no redireccionamos luego del envío del formulario, deberíamos limpiar los campos del mismo. Esto requiere acceder a una función reset()
que forma parte de react-hook-form
pero no disponible cuando usamos un simple <Form>
(como lo estamos usando actualmente).
react-hook-form
tiene un hook llamado useForm()
que es llamado dentro del <Form>
. Para restablecer el formulario debemos invocarlo explícitamente. Pero la funcionalidad de useForm()
debe usada también en el Form
. Así lo haremos.
Primero importemos useForm
:
Luego lo llamamos dentro del componente:
Finalmente le diremos al <Form>
que use los métodos formMethods
que acabamos de crear en lugar de los predeterminados:
Ahora podemos llamar a reset()
en formMethods
después de llamar toast()
:
Ya puede volver a colocar la validación del correo en el
<TextField>
, pero debería mantener la validación del lado servidor por si acaso.
Así queda la página ContactPage.js
:
¡Eso es todo! React Hook Form proporciona un montón de funcionalidades que un <Form>
común no tiene. Para usar esas funcionalidades puede: llame a useForm()
pero asegúrese de pasar el objeto (el que llamamos formMethods
) como una propiedad al <Form>
para que la validación y otras funcionalidades sigan funcionando.
Puede que haya notado que la validación del formulario en el evento onBlur dejó de funcionar al llamar explícitamente a
useForm()
. Eso es porque Redwood llama auseForm()
detrás de las escenas y automáticamente pasa la propiedad devalidación
del<Form>
. Redwood no llama más auseForm()
, por lo que necesitará pasar opciones manualmente:
El sitio público se ve bastante bien. ¿Qué hay de las funciones administrativas para crear y editar posts? Deberíamos moverlos a una sección de administración y requerir del inicio de sesión para no venga un usuario cualquier y cambie las URLs para crear anuncios indeseables.