Exploring the Power of React Portals with Example: A Step-by-Step Guide - part 17

 

An explanation of React Portals using a creating tooltip as an example


I've finished the fundamental subject; now that you are familiar with the essential ideas, let's move on to the finest techniques for developing React applications. 

React portals is an advanced method that I'll demonstrate with the creation of a tooltip.

A tooltip is a text box that appears when you hover your mouse over an element on a web page. We usually use the CSS property z-index to position it at the top. However, react works in component hierarchy, where it maintains parent-child relationships in the DOM tree structure, so you must track component hierarchy whenever adding a layer over another component.

Before proceeding, we need to grasp DOM because we'll be using the react-dom package.

DOM Hierarchy

DOM is a shortened form for Document Object Model, which is a modal of HTML structure in a node tree that represents the entire UI of a web page. When we use state and update data in our React application, the DOM also updates and represents that data in the UI. Every element we add is a node in the DOM tree, also refer to as a tree data structure.

The structure where a component appears within another component is known as the component hierarchy. To put it simply, when you add a component, it adds as a node in the DOM tree that renders on the screen.

Representation of the real DOM and React DOM


In this section, we will not work with the root DOM but rather outside of it, but first, let me create a tooltip with CSS.

React tooltip on hover

We'll create a simple tooltip in React and use React portals to add it to the DOM later in this section.

Create tooltip component

When a button hovers, I want a tooltip to appear. To begin, create a component file called ToolTip and declare an arrow function that returns a div.

ToolTip.js

const ToolTip = () => {
  
  return (
    <div>
      <div className="tooltip-label">I have message for you!</div>
    </div>
  )
}

Use CSS to style tooltip

In order to accomplish the desired outcome, we will now apply the style. To do this, we will create a tooltipStyle object and pass it inside the style attribute of the ToolTip component.

 ToolTip.js

const tooltipStyle = {
  position: 'absolute',
  backgroundColor: '#000',
  color: '#fff',
  padding: '1rem',
  borderRadius: '6px',
  font: '700 1rem sans-serif',
}
return (
  <div style={tooltipStyle}>
    <div className='tooltip-label'>
      I have message for you!
    </div>
  </div>
)

Import tooltip component into the App

To use this with a button, we'll import this component into the App component. I use div to enclose both the button and the tooltip component and establish the style parameters so that the tooltip only applies to this element.

App.js

import ToolTip from './ToolTip'
function App() {
  
  return (
    <div className="App"> 
      <h1>Display Image In React</h1>
      <div style={{position: "relative", display: "inline-block"}}>
        <button type="button">Button</button>
        <ToolTip />
      </div>
    </div>
  )
}

Use useState hook 

The following step is to use the useState hook with a false initial value so that tooltip should no longer be visible on the screen. We must lift state up and declare the state variable in this App component because the ToolTip component is loaded into it.

App.js

import { useState } from "react"

function App() {
  const [tooltip, setTooltip] = useState(false)

  return (
    ....
    {tooltip && <ToolTip/>}
  )
}

Use mouse event to control the tooltip

In order to control when the tooltip appears, we must first listen for mouse events. If the mouse arrow enters the button region, we will update the function and listen for the onMouseEnter event; otherwise, the onMouseLeave event will be triggered when the pointer leaves the button area.

import ToolTip from './component/ToolTip'
import { useState } from "react"

function App() {
  const [tooltip, setTooltip] = useState(false)
  const handleMouseOn = () => {
    setTooltip(true)
  }
  const handleMouseOut = () => {
    setTooltip(false)
  }
  return (
    <div className="App"> 
      <h1>Display Image In React</h1>
      <div style={{position: "relative", display: "inline-block"}}>
      <button type='button'
      onMouseEnter={handleMouseOn}
      onMouseLeave={handleMouseOut}>Button</button>
      {tooltip && <ToolTip />}
      </div>
    </div>
  )
}

If you were coding with me, you'll notice that it works fine, so the question is, what are react portals for?

React portals overflow hidden

It is not a good idea to include an overlay component within a component hierarchy structure. An overlay is like an extra layer on top of all of the components; if the parent has a style overflow: hidden, it will affect the child's appearance and will be limited to the visible area of the parent node.

Perhaps the z-index can help in this situation, but it will not always work, especially in large react applications.

A component or element added to the DOM nodes in React will be added to its parent.

This is why React introduces Portals, which allow us to render elements outside of the component hierarchy without breaking the parent-child relationship.

What is Portals in React?

Portals are an advanced React concept allowing React to render child components outside the default tree structure. In this case, the child component will not be limited to the parent's DOM node and will align with the DOM tree while avoiding overflow.

We use the createPortal method, a DOM-specific method imported from the react-dom library. The child component is still added to the DOM node that is not nested inside but rather outside in another location in the DOM  and it can be modified using the state and properties of the parent component.

Implement React Portal

We'll use portal align with the DOM tree to place the tooltip directly in the document's body. To portal the ToolTip component, we must perform the following action.

  1. Add the portal DOM node where the tooltip renders outside of React root DOM.
  2. call the createPortal method with JSX child component and the DOM node, we specified.

Step - 1: react portals mount render children

If you go to the public folder and open the index.html file, you'll see a div with the root id that contains the entire application.

<div id="root"></div>

Next to the root id, we'll declare a separate DOM node that will be nested inside the body.

<div id="root"></div>
<div id="overlay-root"></div>

When we open devtool, we see two DOM trees: the root DOM, which contains all of the components, and the portal DOM node, which contains the tooltip component.

Step - 2: Rendering child component to a different part of the DOM

We'll now use the createPortal method, which takes two arguments: the JSX element to render outside of the default tree and the DOM location.

The method exists within the react-dom library, so we must import it from there and return it within JSX. I'll wrap my tooltip in another component so that it's easy to pass into the createPortal method.

import { createPortal } from 'react-dom'

const ToolTip = () => {
  const TooltipOverlay = () => {
    return <div style={tooltipStyle}>
     <div className="tooltip-label">click to open the form</div>
    </div>
  }
  return ()
}

I'll enclose the function in curly braces in the JSX return statement and pass the TooltipOverlay component we previously defined as children. As a second argument, we'll utilize a DOM element to gain access to the overlay-root, which is where we want to render this JSX.

return (
  <>
    {createPortal(<TooltipOverlay />, document.getElementById('overlay-root'))}
  </>
)

To wrap my method, I use Fragment (<></>). We were able to successfully relocate the ToolTip component outside of the DOM hierarchy.

Calculating the Tooltip Position 

When we break down the component hierarchy, the tooltip is no longer related to the button element; therefore, we must calculate the tooltip's position relative to the button element so that it appears in the correct location on the screen. 

I'll remove the relative style that surrounds the button and tooltip because it's no longer required, check the whole code at the bottom.

Here, we'll store a reference to the DOM node of the button element in a state variable and pass the target prop from the parent component to the ToolTip component, this will help position the tooltip.


const [nodePosition, setNodePosition] = useState(null)
------
{tooltip && <ToolTip target={nodePosition} />}

Inside handleMouseOn, call the setNodePosition function and pass the button DOM node as an argument to get the current value of the button's top and left properties.

const handleMouseOn = (e) => {
  setTooltip(true)
  setNodePosition(e.target)
}

If the mouse leaves the button region, the position will be reset to null.

const handleMouseOut = () => {
  setTooltip(false)
  setNodePosition(null)
}

The final step will be to set the top and left positions within the style to dynamically set the position of the tooltip based on the position of the button.

const tooltipStyle = {
    position: "absolute",
    backgroundColor: "#000",
    color: "#fff",
    padding: "1rem",
    borderRadius: "6px",
    font: "700 1rem sans-serif",
  }
  if (target) {
    tooltipStyle.top = `${target.offsetTop + target.offsetHeight}px`
    tooltipStyle.left = `${target.offsetLeft + target.offsetWidth/2}px`
    tooltipStyle.transform = 'translateX(-50%)'
}

The offsetTop and offsetLeft properties return the distance between the button element and its offset parent's top and left edges.

You can also create a little arrow above the tooltip but to apply pseudo element you need to create another file called ToolTip.css because you can't apply pseudo code in inline style.

ToolTip.css 
.tooltip-label::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 100%;
  margin-left: -5px;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-bottom: 5px solid #000;
}

complete code

App.js
import ToolTip from './component/ToolTip'
import { useState } from "react"

function App() {
  const [tooltip, setTooltip] = useState(false)
  const [nodePosition, setNodePosition] = useState(null)

  const handleMouseOn = (e) => {
    setTooltip(true)
    setNodePosition(e.target)
  }
  const handleMouseOut = () => {
    setTooltip(false)
    setNodePosition(null)
  }

  return (
    <div className="App"> 
      <h1>Display Image In React</h1>
      <button style={{position: 'relative'}} type='button'
      onMouseEnter={handleMouseOn}
      onMouseLeave={handleMouseOut}>Button</button>
      {tooltip && <ToolTip target={nodePosition} />}
    </div>
  )
}

export default App

ToolTip.js
import { createPortal } from "react-dom"
import './ToolTip.css'

const ToolTip = ({target}) => {
  const tooltipStyle = {
    position: "absolute",
    backgroundColor: "#000",
    color: "#fff",
    padding: "1rem",
    borderRadius: "6px",
    font: "700 1rem sans-serif",
  }
  if (target) {
    tooltipStyle.top = `${target.offsetTop + target.offsetHeight}px`
    tooltipStyle.left = `${target.offsetLeft + target.offsetWidth/2}px`
    tooltipStyle.transform = 'translateX(-50%)'
  }
  const TooltipOverlay = () => {
    return <div style={tooltipStyle}>
    <div className="tooltip-label">click to open the form</div>
  </div>
  }
  
  return (
    <>
      {createPortal(<TooltipOverlay />, document.getElementById('overlay-root'))}
    </>
  )
}

export default ToolTip

Conclusion

This article covered the fundamentals of React portals and how you can use them to build better React applications. It enables the rendering of Modal, Tooltip, and floating elements outside of the main component tree, allowing you to display them without interfering with the rest of the page layout.

0 Comments:

Post a Comment