cover

Full-stack components | modo1: API route


robot logo saying hello

No te quedes atrás: Actualízate

Suscríbete para recibir información sobre nuevos frameworks, updates, eventos, tips, hacks y más.

Estaba yo de chismoso paseando por el blog de algunos de los líderes en internet con respecto a las tendencias y frameworks web, como Kent C. Dodds. Y me encontré con un sentimiento chistoso.

Kent nos presenta en su blog un concepto muy interesante, algo que él llama “full stack components”.

Y te digo que me provocó un sentimiento chistoso, porque es algo que desde que comencé a utilizar Remix he venido haciendo sin darme mucha cuenta, y ahora que Kent le da un nombre, me he hecho consiente de ello, de que he estado diseñando full-stack components.

Y en este video quiero compartirte la primera de varias versiones que ire compartiendo contigo. 🤓

Para este primer componente full-stack vamos a crear un menu de cuenta simple, es decir, un componente que coloquemos en la barra de navegación y le permita a nuestro usuario abrir y cerrar su sesión, también navegar a su perfil.

Así que si esto te interesa, pues ¡vamos a darle! 👷🏽🧑🏼‍🏭

Agrega Tailwind con un comando?

Estructura del proyecto

Vamos a clonar el repo de este ejercicio para que sea aún más fácil para ti seguirme en este ejemplo, el link ya sabes, está allí debajo del video. Y la branch es: start.

clonamos el proyecto:

git clone https://github.com/FixterGeek/Youtube_fullstack_components.git

Entramos, cambiamos la branch e Instalamos todo lo necesario

cd Youtube_fullstack_components git checkout start npm i

Y observamos las dos rutas que ya contiene el proyecto.

👀 Ejecuta el servidor de desarrollo del proyecto para vivitar la home page: npm run dev.

Creando la barra de navegación

Vamos a crear una barra de navegación que se dibuje en el archivo root.tsx de nuestro proyecto, este archivo hace la función de layout general del sitio web, así que ahí colocaremos nuestra NavBar para que la compartan nuestras dos rutas (/ y /dash).

import { Link } from "@remix-run/react"; import { TbBell } from "react-icons/tb"; export default function NavBar() { return ( <nav className="bg-gradient-to-r from-indigo-500 to-violet-500"> <div className="max-w-3xl flex items-center gap-4 py-4 px-4 mx-auto text-white text-sm"> <Link to="/"> <img className="w-[140px]" src="https://www.logodesign.net/images/home-logos/brand-amazon-logo.png" alt="logo" /> </Link> <span className="text-black bg-white p-1 rounded-full text-lg ml-auto"> <TbBell /> </span> {/* <AccountManager /> */} </div> </nav> ); }

Esta barra nos será suficiente para lo que queremos conseguir. Es hora de pasar a lo bueno, vamos a crear nuestro componente full-stack que nos ayude a mostrar diferentes opciones según nuestro usuario esté “logueado”.

Creando el componente full stack

Nuestro componente se llamará AccountManager a falta de un mejor nombre.

Este componente hace varias cosas y le llamamos full-stack porque no solo se encarga de mostrar datos en el cliente, también de modificarlos o mutarlos en el servidor. Por medio claro, de nuestra ya conocida Fetch API. Vamos a hacer uso del poderoso fetcher de Remix para lograr comunicarnos con el servidor con confianza.

// app/components/AccountManager.tsx const AccountManager = () => { // 1. Get user info // 2. Signout action // 3. Redirect action // if logged redirect to profile // else redirect to /login }

Necesitamos completar estas 5 tareas, vamos a realizarlas una por una.

1. Consiguiendo al usuario desde el servidor

Nuestro componente necesita saber si el usuario tiene una sesión iniciada. Para ello vamos a consultarla desde el API que en unos momentos escribiremos, primero escribiremos el componente del cliente.

import Dropdown from "./Dropdown"; import { Menu } from "@headlessui/react"; import { useFetcher } from "@remix-run/react"; import { useCallback, useEffect, useState } from "react"; type UserType = { email: string; }; const AccountManager = () => { // 1. Get user info const fetcher = useFetcher(); const [user, setUser] = useState<UserType | null>(null); const getUser = useCallback(async () => { fetcher.load("/api/auth"); if (fetcher.data?.ok) { setUser(fetcher.data.user); } }, [fetcher]); // checking for fetcher useEffect(() => { if (fetcher.data && fetcher.data.ok) { setUser(fetcher.data.user); return; } }, [fetcher]); // run once, on the beginning useEffect(() => { getUser(); }, []); // eslint-disable-line // 2. Signout action // 3. Redirect action // if logged redirect to profile // else redirect to /login return ( <> {user && <p>{user.email}</p>} {!user && <button>Inicia sesión</button>} <Dropdown cta={ user ? ( <img className="w-[40px] rounded-full " src="https://avatars.githubusercontent.com/u/7883990?v=4" alt="user pic" /> ) : null } > <Menu.Item> {() => ( <button className="py-2 px-2 rounded-sm hover:bg-gray-200 text-black text-md font-medium" > Manage account </button> )} </Menu.Item> <Menu.Item> {() => ( <button className="py-2 px-2 rounded-sm hover:bg-gray-200 text-black text-md font-medium" > Sign out </button> )} </Menu.Item> </Dropdown> </> ); }; export default AccountManager;

Toma nota del uso de un par de componentes que vienen de la biblioteca HeadlessUI. Estoy usando específicamente el componente Dropdown.

4. Escribiendo el API

Es importante escribir el action que se encargará de estas peticiones que hará el fetcher de nuestro componente. Por ello, vamos a crear una ruta de recursos en Remix.

// app/routes/api.auth.tsx import type { ActionFunction } from "@remix-run/node"; export const action: ActionFunction = async () => { return null; };

Esto es todo por ahora, volveremos a este archivo en un momento, primero terminemos con el componente del cliente.

2. Escribiendo las acciones (signout)

Vamos a escribir una función que utilizará también el fetcher para hacer una petición, esta vez de tipo post. Lo que solicitará al API el cierre de sesión. el Servidor se encargará del redireccionamiento, así que no tenemos más que hacer en el cliente para esta acción.

const handleSignout = () => { fetcher.submit( { intent: "signout" }, { method: "post", action: "/api/auth" } ); };

¿Apoco no sientes el poder y el dinamismo del fetcher? 🔥

No olvidemos asignar esta función al componente correspondiente.

<Menu.Item> {() => ( <button onClick={handleSignout} className="py-2 px-2 rounded-sm hover:bg-gray-200 text-black text-md font-medium" > Sign out </button> )} </Menu.Item>

3. Segunda acción del menú

La segunda acción disponible en el menú es dinámica. Si el usuario está presente lo redireccionaremos a la ruta /profile, pero si no, lo llevaremos a home /.

// 3. Redirect action const handleLink = () => { if (user) { navigate("/profile"); } else { navigate("/"); } };

Tampoco olvidamos agregar esta función al onClick del botón correspondiente.

<Menu.Item> {() => ( <button onClick={handleLink} className="py-2 px-2 rounded-sm hover:bg-gray-200 text-black text-md font-medium" > Manage account </button> )} </Menu.Item>

De esta manera, hacemos que nuestra función sea dinámica en lugar de que lo sea el JSX. Aunque si por accesibilidad prefieres usar el componente Link 👏🏼 ¡Venga tienes razón, puedes mejor usar el JSX dinámico!

4. Handler para el login

Necesitamos una función que solicite un login (fake) a el action de la ruta api/auth.

// 4. login user const handleLogin = () => { fetcher.submit( { intent: "login" }, { method: "post", action: "/api/auth" } ); };

Y por tercera vez, no olvidar asignar esta función al botón correspondiente.

return ( <> {!user && <button onClick={handleLogin}>Entrar</button>} // ... )

Con esto nuestro componente está listo, lo único que hace falta es escribir el api para resolver cada una de estas dos peticiones: iniciar sesión y cerrar sesión.

5. Escribiendo la API

Nuestra API no es más que un archivo con una exportación, la función action. Lo que la convierte en una especie de ruta de recursos o api endpoint, ya la hemos creado antes, ahora es momento de desarrollarla; vamos.

import { type ActionFunction, type LoaderFunction, json, } from "@remix-run/node"; import type { UserType } from "~/components/AccountManager"; import { commitSession, getSession } from "~/sessions"; export const action: ActionFunction = async ({ request }) => { const formData = await request.formData(); const intent = formData.get("intent"); const session = await getSession(request.headers.get("Cookie")); if (intent === "login") { session.set("userId", "fixtergeek@gmail.com"); // change for DB info return json( { user: { email: "fixtergeek@gmail.com" } }, { headers: { "Set-Cookie": await commitSession(session), }, } ); } if (intent === "signout") { session.unset("userId"); return json( { user: null }, { headers: { "Set-Cookie": await commitSession(session), }, } ); } return null; };

Esto nos permite abrir y cerrar sesión, ahora necesitamos un loader para devolver los datos del usuario en caso de que su sesión esté iniciada.

6. Loader API

type LoaderData = { ok: boolean; error?: string; user?: UserType; }; export const loader: LoaderFunction = async ({ request }) => { const session = await getSession(request.headers.get("Cookie")); if (session.has("userId")) { const email = session.get("userId"); return { ok: true, user: { email } } as LoaderData; } return { ok: false, error: "No active session" }; };

Este es nuestro loader. Observa que el return tiene una forma similar al del action, con esto podemos administrar bien las respuestas de nuestra API en el cliente. Vamos a hacerlo.

7. Escribiendo la lógica del fetcher

Vamos a manejar todas estas idas y vueltas con un par de bloques de código dentro de un useEffect. Queremos observar todas las peticiones del fetcher y ejecutar un efecto secundario cuando detectemos que la sesión ha cambiado.

// Fetcher observer useEffect(() => { if (fetcher.data && fetcher.data.ok) { setUser(fetcher.data.user); } else { setUser(null); } }, [fetcher]); // run once, on the beginning useEffect(() => { getUser(); }, []); // eslint-disable-line

Usamos useEffect dos veces en este código, el primero observa las respuestas que nos devuelve el API y el segundo se ejecuta solamente al montarse el componente (solicitando la info del usuario).

Observa que preguntamos si el api devolvió datos (.data) si es así entonces devolvió al usuario y lo colocamos en el estado local. Si no, entonces lo borramos.

👀 No tenemos que preocuparnos por el redireccionamiento, cuando fetcher termina la petición, 💿 Remix llama al loader de la ruta automáticamente. Lo cual resulta sumamente útil para volver a validar la cookie. 🍪🥠

¡Y ya está, Lo logramos! Ahora tenemos un componente full-stack que podemos usar en cualquier lugar de nuestro sitio web y no necesita prop alguno. 🔥😎

Espero esto te sea muy útil y te animes a construir tus propios componentes full-stack. 🛠️

Abrazo. blissmo. 🤓

Enlaces relacionados

Repositorio del proyecto:

https://github.com/FixterGeek/Youtube_fullstack_components

Rutas API en Remix:

banner

¿Quieres mantenerte al día sobre los próximos cursos y eventos?

Suscríbete a nuestro newsletter

Jamás te enviaremos spam, nunca compartiremos tus datos y puedes cancelar tu suscripción en cualquier momento 😉

robot logo saying hello
facebook icontwitter iconlinkedin iconinstagram iconyoutube icon

© 2016 - 2023 Fixtergeek