React — Portales

Mauricio Garcia
10 min readFeb 15, 2021

--

Temario

  • El nodo DOM principal de React
  • ¿Qué son los portales?
  • Creando un modal básico
  • Creando customs hooks (usePortal, useModal)

i. El nodo DOM principal de React

Cuando vimos “React — Primeros pasos…[ref]”, explicamos qué React SIEMPRE va a renderizar todo el contenido sobre un nodo DOM:

En la mayoría de los proyectos es un nodo div con id="root", aunque dependiendo la configuración puede variar. Por ejemplo en las aplicaciones hechas con create-react-app podemos ir al archivo src/index.js, para revisar su punto de montaje:

Podemos observar que va a renderizar todos los componentes dentro del nodo DOM div[id="root"].

Ahora el archivo public/index.html:

Observamos el elemento div con su id="root".

Si revisamos en consola algún proyecto, observaremos que todo está renderizado dentro de ese nodo DOM.

Todos los ejemplos que se han hecho hasta este momento no se ha tenido ningún problema de que todo esté en un solo nodo, pero, imagina el siguiente escenario:

Tenemos un carrito de compras que muestra un catálogo de ropa, donde, al darle click a un producto nos va abrir un modal con la descripción completa de dicho producto, algo así:

Los componentes deben verse parecido a la siguiente imagen:

La estructura realmente no está mal, aunque es posible que nos pueda generar MUCHOS problemas el componente Modal, ya que tiene reglas de CSS que no sonnaturales” como position:fixed, top:0, left:0, etc... y ese tipo de propiedades dan un dolor de cabeza dentro de una aplicación.

Para solucionar este tipo de problemas, los desarrolladores de React crearon los portales

ii. ¿Qué son los portales?

Los portales proporcionan una opción de primera clase para renderizar hijos en un nodo DOM que existe por fuera de la jerarquía del DOM del componente padre.[ref]

:: ¿Cómo funciona?

Cuando renderizamos un componente en React, se ve de la siguiente manera:

Podemos observar en la animación, al momento de darle click a un ítem, lo que hace React es crear un nuevo nodo dentro del componente y cuando lo cerramos se desmonta.

Cuando renderizamos un componente en React, pero utilizando los portales, se ve de la siguiente manera:

Podemos observar en la animación, al momento de darle click a un ítem, lo que hace el portal de React es crear un nuevo nodo fuera de nodo[id="root"], y agregar el componente y cuando lo cerramos se desmonta.

Ventajas de usar portales:

  • Evitamos dolores de cabeza con los estilos de CSS, ya que está al mismo nivel que nodo[id="root"].
  • No importa que este renderizado fuera del árbol de React nodo[id="root"], ya que se sigue comportando como si estuviera dentro.
  • Soporta todas las propiedades, eventos, hooks, etc. de un componente normal.
  • Puede interactuar con el componente padre.

:: Sintaxis

iii. Creando un modal básico

La idea es tener un botón, y cuando se le dé click debe mostrar un modal.

:: Sin portales de React

Primero vamos a hacer el ejemplo sin portales.

Vamos a la carpeta donde están nuestros proyectos y desde ahí ejecutamos:

Si tienes dudas por que npx o llegaste aquí directo, te recomiendo que inicies primero por acá (Mi primera App con create-react-app [ref])

Para los estilos utilizaremos emotion[ref], entonces, vamos a la carpeta del proyecto, y lo instalamos:

Puedes utilizar el que más te guste, revisa esta story para conocer más (React — Formas de diseñar componentes de React, desde estilos en línea hasta CSS in JS[ref])

Vamos a crear la carpeta src/Components y dentro de ella el componente Container:

Y lo agregamos a la App:

Continuamos en el componente Container, vamos a crear un botón y en su atributo onClick agregamos la función handleModal:

Importamos emotion y agregamos estilos al botón:

El siguiente paso es crear el componente src/Modal, donde va a recibir las siguientes propiedades:

  • children — El componente que React va a renderizar.
  • onCloseFunción que va a invocar cuando se le dé click a X(cerrar).
  • openBooleano que nos va a servir para saber si debemos renderizar o no el componente Modal.

En el componente Container, importamos el componente Modal y lo agregamos:

A continuación haremos lo siguiente:

  • (A) — Crear el estado openModal, para mostrar/ocultar el componente Modal.
  • (B) — Vamos a crear la función toggleModal para cambiar el estado.
  • (C) — Dentro de la función handleModal ejecutamos la función toggleModal.
  • (D) — Al componente Modal le pasamos los atributos open={openModal} y onClose={toogleModal}.

Si hemos hecho todo bien debemos de ver lo siguiente:

Regresamos al componente Modal, importamos emotion y agregamos estilos:

Revisamos de nuevo la aplicación:

Podemos observar en la animación, que está renderizando el componente dentro del nodo principal.

— Ejemplo completo

:: Con portales de React

Vamos agregar el portal de React al ejemplo:

En el componente Modal vamos a importar ReactDOM

De acuerdo a la sintaxis el código quedaría de la siguiente manera:

Donde:

Revisamos de nuevo la aplicación:

Podemos observar en la animación, que está renderizando el componente fuera del nodo principal.

Sencillo ¿no?…

— Ejemplo completo

iv. Creando customs hooks (usePortal, useModal)

Vamos a crear dos hooks para crear un modal. Que sea tan sencillo como:

Donde, solo es necesario invocarlo y utilizarlo. Y tan avanzado como:

Donde, lo invocamos y aprovechamos los métodos que expone para montar/desmontar el componente.

Antes de hacer el hook del modal, vamos a crear el hook del portal

:: Creando la app con create-react-app

Lo primero que vamos a hacer es ir a la carpeta donde tenemos los proyectos, y desde ahí ejecutamos el siguiente comando:

Crearemos dos carpetas src/Components y src/Hooks.

:: Creando el hook usePortal

Dentro de la carpeta src/Hooks vamos a crear el hook usePortal :

Vamos a recibir de entrada dos propiedades :

  • containerId[opcional]— El id del contenedor portal (default use-portal-react)
  • defaultShow[opcional]— Si se va a renderizar el componente de entrada o hasta una acción (default true)

Y vamos a crear el estado isShow, con la finalidad de renderizar el portal:

Afuera del componente vamos a crear una closure[ref] llamada portal, donde la primera función va a recibir containerId, defaultShow y la segunda función { children } que va a retornar children :

Vamos a invocar la función portal, dentro del hook useCallbak[ref**] (con la finalidad de memorizar la función):

Probemos lo que llevamos hasta aquí. Nos vamos a la App, importamos el hook, y lo utilizamos:

Si hemos hecho todo bien, debemos de ver lo siguiente:

Vamos a explicar paso a paso:

Continuemos…

El siguiente paso es crear el portal con createPortal de react-dom:

Podemos observar en la imagen que el portal está funcionando, pero… el componente quedamuy suelto”, entonces, vamos a encapsular el componente dentro de un div. Regresamos al hook usePortal y hacemos los siguientes cambios:

Donde:

  • (A) — Vamos a crear un estado container, para almacenar el contenedor div.
  • (B) — Cómo vamos a crear dinámicamente el contenedor, vamos a usar el hook useEffect.
  • (C) — La dependencia que va a tener es el estado container, esto quiere decir que cuando cambie, se va a ejecutar lo que está dentro.
  • (D) — Si existe el contenedor es el que usamos, caso contrario vamos a crear uno invocando la función createEl.
  • (E) — Función que va a crear el contenedor y será agregado al document.body.
  • (F) — En caso de que no exista el contenedor o se haya desmontado, no vamos a retornar nada.
  • (G) — Hack para validar el momento que se haya desmontado y poder eliminar el contenedor (Recuerda que lo estamos haciendo con JavaScript y no con React).
  • (H) — Validamos si isShow = true, existe el container crea el portal y lo regresa.

Si hemos hecho todo bien, debemos de ver lo siguiente:

Podemos observar en la imagen que el portal se ha creado dentro de un div.

El siguiente paso es exponer dos funciones en el hook usePortal, para que el desarrollador pueda renderizar o eliminar el componente, entonces:

Vamos a la App y agregamos los botones para interactuar:

Si hemos hecho todo bien, debemos ver lo siguiente:

El siguiente paso es recibir dos funciones, con la finalidad de avisar al usuario si se ha renderizado o se ha desmontado, entonces:

Agregamos las propiedades onShow y onHide y en las funciones correspondientes las ejecutamos:

Agregamos las funciones en la App :

— Ver ejemplo completo

Nota: He utilizado useCallback para memorizar los métodos, tanto de App como de usePortal.

— Descargar proyecto

Puedes descargar el proyecto de GitHub[ref] con el tag usePortal

:: Creando el hook useModal

Dentro de la carpeta src/Hooks vamos a crear el hook useModal :

Vamos a recibir de entrada cuatro propiedades :

  • containerId [opcional]— El id del contenedor portal (default use-modal-react-portal).
  • defaultShow[opcional] Si se va a renderizar el componente de entrada o hasta una acción (default false).
  • onShow [opcional]—Método que se ejecuta después de renderizar el componente.
  • onHide [opcional]—Método que se ejecuta después de desmontar el componente.

Vamos a importar el hook usePortal:

Ahora en la App debemos cambiar usePortal por useModal y <Portal> por <Modal>

Si revisamos la aplicación, no debemos tener ningún cambio:

Afuera del componente vamos a crear una closure[ref] llamada modal, donde la primera función va a recibir Portal, isShow, hide y la segunda función { children } , que va a retornar <Portal>{children}</Portal> :

Vamos a invocar la función modal, dentro del hook useCallbak[ref] (con la finalidad de memorizar la función):

Vamos a darle un poco más de forma al componente:

Ahora, para los estilos vamos a usar CSS Module[ref], agregamos los estilos en un nuevo archivo src/Hooks/useModal.module.css:

Importamos dentro del hook useModal, y agregamos:

Si revisamos la aplicación, debemos ver lo siguiente:

El siguiente paso es permitirle al desarrollador, que pueda quitar o dejar la x del modal, entonces:

Donde:

  • (A) — Agregamos como propiedad showClose (por default true).
  • (B) — Lo pasamos a la función modal.
  • (C) — Recibimos el atributo showClose y hacemos la validación.

En la App, ya podemos hacer que se oculte:

El siguiente paso es permitirle al desarrollador, que se pueda desmontar el componente si le damos click fuera del modal:

Donde:

  • (A) —Agregamos como propiedad clickOutsideToHide (por default false).
  • (B) —Lo pasamos a la función modal.
  • (C) — Recibimos el atributo clickOutsideToHide.
  • (D) — Vamos a usar el hook useRef, para guardar la referencia del div wrapper.
  • (E) — Hacemos la validación, para que desmonte el componente, y agregamos la referencia del div.
  • (F) — La función va a validar que el elemento que se le dió click sea igual al de la referencia, de ser cierto ejecuta hide, caso contrario ignora.

En la App:

Podemos observar en la animación que desmonta el componente hasta que se le da click fuera del modal.

— Ver ejemplo completo

— Descargar proyecto

Puedes descargar el proyecto de GitHub[ref] con el tag useModal

--

--