React — Portales
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 son “naturales” 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 quenodo[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.onClose
— Función que va a invocar cuando se le dé click aX
(cerrar).open
— Booleano que nos va a servir para saber si debemos renderizar o no el componenteModal
.
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 componenteModal
. - (B) — Vamos a crear la función
toggleModal
para cambiar el estado. - (C) — Dentro de la función
handleModal
ejecutamos la funcióntoggleModal
. - (D) — Al componente
Modal
le pasamos los atributosopen={openModal}
yonClose={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]— Elid
del contenedor portal (defaultuse-portal-react
)defaultShow
[opcional]— Si se va a renderizar el componente de entrada o hasta una acción (defaulttrue
)
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 queda “muy 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 contenedordiv
. - (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 deApp
como deusePortal
.
— 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]— Elid
del contenedor portal (defaultuse-modal-react-portal
).defaultShow
[opcional] — Si se va a renderizar el componente de entrada o hasta una acción (defaultfalse
).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 defaulttrue)
. - (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 defaultfalse)
. - (B) —Lo pasamos a la función
modal
. - (C) — Recibimos el atributo
clickOutsideToHide
. - (D) — Vamos a usar el hook
useRef
, para guardar la referencia deldiv
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
En la siguiente entrega vamos a ver React — React Router
La entrega pasada vimos React — Componentes genéricos con props.children y la API Children
Bibliografía y links que te puede interesar…
Iconos e imágenes…