Lecciones del curso

Introducción al desarrollo web full stack con React Router

RRv7 como puente a React 19
5m
Todo sobre rutas
6m
Todas las piezas de un Route Module
7m
Cargando datos desde la base de datos
4m
Actions y mutaciones
7m
Componente <Link> y navegación
5m
UI Patterns: Pending & Optimistic
4m
¿Cómo sustituir un useEffect?
3m
Tipado seguro de extremo a extremo
1m
Testing con RRv7
1m
Instalación
1m
Estrategias de renderizado
3m

Actions y mutaciones

Todo el tiempo escuchamos hablar de mutaciones pero ¿De qué estamos hablando realmente, hablamos de zombies o de mutantes? Porque, los zombies de 28 días después no son zombies, son infectados. 🧌 

Fuera de broma, de lo que realmente se habla es simplemente de nuestras peticiones POST, PUT, PATCH o DELETE que modifican nuestros objetos de la base de datos. ✅

Sabemos que podemos usar un POST para agregar un nuevo objeto a nuestros modelos, un nuevo producto, por ejemplo, o un nuevo usuario. También sabemos que podemos usar PATCH para actualizar algún campo de ese objeto usuario o producto y que DELETE lo eliminará (aunque en la actualidad nada se borra, nomás se marca como “borrado”). Pero sí, tenemos todos estos métodos HTTP a la mano para nuestras mutaciones. ¿Sabías que una etiqueta <form> de HTML solo soporta las peticiones GET y POST? Sí, las otras solo las usa el método fetch de la plataforma. Bueno, aquí es donde entra nuestra función action: ayudándonos a recibir todas estas peticiones que no son GET. 🌧️

Acciones en el cliente

Como tal vez ya lo mencionamos en los videos anteriores de este curso, la función clientAction solo se ejecuta en el navegador (en el cliente). Y tiene prioridad sobre la función action del servidor. 😎

export async function clientAction({request}) { const formData = await request.formData(); // igualito que en el server const data = Object.fromEntries(formData); const response = await updateVideo(data); return { ok: response.ok } }

En este ejemplo, utilizamos una función updateVideo que por dentro utilizará fetch para comunicarse con nuestro API o con cualquier otra API de terceros.

👀 Cuando llamemos a nuestras acciones, primero se llamará al clientAction, que a su vez puede llamar, si lo necesitase, al action del servidor, pues recibirá una función en sus parámetros. ⛈️

Acciones del servidor

Estas acciones se ejecutan solamente en el servidor y son muy útiles para acceder a la base de datos directamente sin la necesidad de una REST API. 👏🏼

export async function action({request,params}) { const formData = await request.formData(); const data = Object.fromEntries(formData); const video = await db.vide.update({ where:{ id: params.id }, data }); return { video, ok: true } // puede coincidir o no. }

Lo que devuelve la función action estará disponible en el componente a través del prop actionData.

export default function Route({actionData}) { return (<>{actionData}</>) }

Así, en nuestro cliente podremos concentrarnos en mostrar estos datos e informar al usuario; en vez de estar administrando estados globales duplicados. 😓

👀 Recuerda que todo el código que coloquemos en nuestra función action será removido del bundle del cliente y apartado junto a todo el código del servidor. 🗂️

Llamando a nuestras acciones

¿Cómo y cuando se usan estas funciones? Pues, el nombre debe orientarnos un poco; nos comunicaremos con estas funciones cuando queramos realizar una acción en nuestra base de datos. ✅

Seguro sabes que hay varias maneras de escribir código, entre las cuáles se encuentra la manera declarativa: puedes pensar en JSX o HTML. Pero, también podemos escribir código imperativo: aquí pensamos en los hooks. Con React Router podemos usar el estilo que más nos satisfaga o hasta combinarlos.

Para emplear nuestras actions de forma declarativa tenemos a la mano el componente <Form> o <fetcher.Form>; y para usarlas de forma imperativa podemos ocupar useSubmit y fetcher.submit. Veamos un ejemplo de cada uno ¿te parece? 🤓📚

👀 Cuando una función action se completa (o termina), la función loader de la ruta es llamada para revalidad los datos que se están leyendo en la ruta actual y así mantener la interfaz sincronizada sin necesidad de escribir código para ello. 😎

Llamando acciones con un formulario JSX

Esta manera es mi favorita. Yo, intento emplearla lo más que puedo. Cada que evito lo imperativo, estoy siendo buena onda con mi yo del futuro que tendrá que mantener todo este código. La manera declarativa le permitirá, a esa versión mía del futuro, comprender más rápido y modificar sin dolor. 👨🏻‍🚀 

import { Form } from "react-router"; export default function Route(){ return ( <Form action="/api/subscriptions" method="post"> <input type="email" name="email" /> <button type="submit">Suscribirme</button> </Form> ); }

Dime que no es obvio lo que aquí pasará. 😊

Es importante mencionar que este método no detiene el comportamiento natural de un <form>, pues: SÍ se detonará la navegación, aunque React Router lo controlará sin refrescar la página, es decir, todo sucederá como en una SPA (single page app) que, por supuesto, es la especialidad de React Router desde hace más de diez años. 👴🏼

Llamando acciones con useSubmit

Claro que, si tú ya tienes escritas unas funciones bien alocadas, 👨🏼‍🎤 y después de masajear tus datos, validarlos o  agruparlos y formatearlos, quieres enviarlos con JS, puedes emplear el hook useSubmit.

import { useCallback } from "react"; import { useSubmit } from "react-router"; import { useTimeout } from "blissmo/utils"; function useQuizTimer(dataState) { const submit = useSubmit(); const { placeTimeout } = useTimeout() const cb = useCallback(() => { submit( { timeout: true, intent:'timeout', data:dataState }, { action: "/api/end-quiz", method: "post" } ); }, [dataState]); placeTimeout(cb,10 * 60 * 1000); // 10m }

La documentación oficial tiene este bonito ejemplo, en el que se le dan diez minutos a esta página para que el usuario pueda responder un quiz (o examen) y enviarlo automáticamente o programáticamente o imperativamente; como prefieras. 🤖 Lo importante aquí es observar el uso de la función submit que nos ha entregado el hook useSubmit.

El primer parámetro que se entrega son los datos que estarán disponibles en el formData tanto del clientAction como del action. Mientras que el segundo argumento es la configuración de la llamada. 📞

De nuevo, esto TAMBIÉN detona la navegación.

Llamando acciones con el fetcher

Por último, una forma híbrida de enviar nuestros formularios a nuestras funciones action es con fetcher. La diferencias entre useSubmit y fetcher son varias, pero una importante es que fetcher NO detonará la navegación. 😲

import { useFetcher } from "react-router"; export default function Route() { const fetcher = useFetcher(); const isFetching = fetcher.state !== "idle"; return ( <fetcher.Form method="post" action="/api/subscriptions"> <input type="email" name="email" /> <button type="submit"> {isFetching ? "Guardando..." : "Guardar"} </button> </fetcher.Form> ); }

Podemos pensar en el fetcher como una manera mixta de trabajar. Combinando lo declarativo del <Form> con lo imperativo del useSubmit. Tendremos el estado de la petición a la mano por si necesitamos usarlo para mostrar spinners. Pero, si necesitamos hacer algo antes de enviar nuestro formulario, podremos pasarnos a su modo imperativo con su propia función submit.

fetcher.submit( new FormData(event.currentTarget), // dentro de onSubmit { action: "/api/subscriptions", method: "post" } );

👀 Aunque estas APIs están pensadas para mutaciones y peticiones POST o PUT, de todas formas podríamos usarlas para hacer consumos GET a cualquiera de nuestros endpoints o rutas. 🤯

Pues ahí está. Una interfaz simple y poderosa para trabajar con mutaciones tanto en el servidor como en el cliente. Aquí nadie te obliga a trabajar por fuerza en el servidor. ⛓️‍💥 A la vez que explotamos los patterns más recomendados y nativos de la plataforma.

Bien bajado React Router. 🧑🏼‍🎤

Abrazo. Bliss. 🤓