How To Work With React Fragments, Portals, And Refs (Part 6)

by kleamerkuri
React refs portals fragments javascript.

Can you believe it? We’ve reached the end of our React app-building journey, and we’ve covered a lot of ground along the way.

From setting up our development environment to building reusable components, adding styles, managing state and handling events, and working with conditional logic, we’ve explored the fundamental concepts of React and built a fully functional web application 🥲

In this final post of our series, we’re going to wrap things up by exploring some advanced React topics: fragments, portals, and refs.

These tools might seem complex at first glance, but once you understand their use cases, you’ll wonder how you ever lived without them.

Okay, that might be somewhat overstated, but they’re super helpful 😌

Fragments allow us to group a list of children without adding extra nodes to the DOM.

Portals provide a way to render children into a different part of the DOM hierarchy, outside the current parent component.

And refs allow us to reference and interact with DOM elements directly, providing even more control over components.

So, get ready to dive into the world of fragments, portals, and refs, and discover how these tools can make your React code more robust and semantically sound.

Let’s end our React app-building journey on a high note and take our skills to the next level!

💫 See full project in action and interact with it on this link.

React Fragments

JSX has limitations, one of them being that you can’t return, or store in a variable, more than one root JSX element.

There are two workarounds:

1) Wrap adjacent elements in a wrapper container div.

If you’ve been following along, this is what’s currently going on in Form.js 👇

return (
    <div className={styles.form_wrapper}>
      {err && <Modal error={errMsg} onError={errHandler} />}
      <form className={`${styles.form} ${errMsg && styles.form__err}`} onSubmit={formSubmitHandler}>
        <label htmlFor="name">Name</label>
        <input
          type="text"
          id="name"
          name="name"
          value={name}
          onChange={nameHandler}
        />

        <label htmlFor="age">Age</label>
        <input
          type="number"
          id="age"
          name="age"
          max="100"
          min="1"
          value={age}
          onChange={ageHandler}
        />

        <Button type="submit">Add User</Button>
      </form>
    </div>
  );


We’re rendering two adjacent components—a modal and form—that are wrapped in a container div.

This method, however, can lead to what my instructor called a “div soup” due to the rendering of unnecessary elements.

Nested divs React.

Its impact is more pronounced in bigger, more nuanced applications.

2) Place adjacent components in an array since React knows how to work with an array of JSX elements.

In such a case, you’ll need to provide key attributes which is a rather cumbersome method.

Neither option is great, so what can we do?

A solution that goes beyond these workarounds is to create a wrapper component that returns props.children. This is an empty component that only renders children.

Tip: Don’t confuse this with wrapper containers like the Card component. In that case, we’re rendering anything wrapped in specific tags!

This wrapper component gets rid of the rendered wrapper divs in the DOM. The end result is just the modal (if there’s an error) and the form – no div or other wrapper encasing them.

Hold on, it gets much better!

You don’t actually need to build such a component since it comes with React as a Fragment 🙌

There are two ways of implementing the rendering of adjacent components:

  1. If supported by the project set-up, you can use <> ... </> (aka “nameless” tags)
  2. Otherwise, import React (if you haven’t already done so) into the file and either use <React.Fragment> or destructure Fragment from the react module and use <Fragment>.

Now, let’s put all this into action!

How to use React Fragments

In Form.js, import Fragment and replace the wrapper div. Remove the className property because we no longer have a wrapper in this component.

Note the class on the div simply added some padding which we are going to keep by transferring the style over to the Card wrapper on App.js.

// App.js
<Card className={styles.form_wrapper}>
   <Form newUser={newUserDataHandler} />
</Card>


Since we pre-defined the Card component to account for any instances of additional styles, we’re all set!

Hint: Copy and paste the associated style to App.module.css! Remove it from Form.module.css as it’s no longer necessary on that component.

Fantastic! The app remains intact but boasts a cleaner base.

Replace nested divs with React fragments.

React Portals

A Portal renders HTML in a different location than its respective JSX placement.

The biggest advantage of Portals is to maintain the semantic HTML organization of a project.

For example, take into consideration components that are separate from, and sometimes lie on top, other components. A modal comes to mind here.

Our current project contains a nested modal that is problematic. Don’t get me wrong, it works, but it’s a bad implementation with poor accessibility.

The modal with its overlay is supposed to lie on top of the root wrapper when it appears. Instead, it’s nested inside the root wrapper, within App, and even further inside the Card wrapper along the rest of the form elements.

Modal component nesting React.

Though not too nested this time around, what if the modal was even deeper in the component tree?

See where I’m going 💁‍♀️

How to use React Portals

A Portal takes two parameters specifying the what and where of the port.

To use a Portal in our project:

  1. Add a “root” in index.js where project overlays can go in. I call this div ”overlay-root”.

Tip 💫
Adding a “root” isn’t necessary as you can port to the body element directly, but “root” divs offer a good way to keep the code base organized.

  1. Import ReactDOM from the react-dom module to access the createPortal method. Likewise, destructure createPortal directly from the module to skip the extra syntax.

The createPortal method takes (a) the JSX component to port and (b) a pointer to the DOM element (using a Vanilla JS selector).

  • The JSX component is the what we’re porting
  • The pointer to the DOM element is the where to place the JSX component

Tip: Store JSX components in variables to make the code clean and reduce possible errors!

// Modal.js
import Button from "./UI/Button";
import Card from "./UI/Card";
import { createPortal } from "react-dom";

import styles from "./Modal.module.css";

const ModalOverlay = (props) => {
  return (
    <div className={styles.modal_overlay}>
      <Card className={styles.modal_wrapper}>
        <div className={styles.modal_header}>
          <h3>Invalid input</h3>
        </div>
        <div className={styles.modal_body}>
          <p>{props.error}</p>
        </div>
        <div className={styles.modal_action}>
          <Button
            type="button"
            className={styles.modal_btn}
            onClick={props.onError}
          >
            Close
          </Button>
        </div>
      </Card>
    </div>
  );
};

const Modal = (props) => {
  return createPortal(
    <ModalOverlay error={props.error} onError={props.onError} />,
    document.querySelector("#overlay-root")
  );
};

export default Modal;


I’m still exporting a Modal component, but it’s now way different.

In fact, there are two components in the Modal file with ModalOverlay holding the main code of the modal.

Modal now acts as the porting agent that makes sure that anytime the Modal component is used anywhere in the project, the ModalOverlay component will render inside of the root div.

Note: I pass the error and onError properties to ModalOverlay when porting in order to access the props values on the component properly!

To sum things up:

  1. Store the modal code in a secondary component. We can’t simply store it in a variable because createPortal requires a JSX variable.
  2. Port the secondary component with the necessary properties to the desired DOM location. Said location must exist in index.html (if not, like in our case, create it).
Using React portal on modal.

React Refs

Refs allow us to access DOM elements so we can work with them by creating a connection between a JSX element and the JS code.

Breaking down the use of refs in three steps:

  1. Import the useRef hook in the same fashion we imported useState
  2. Call useRef() inside of the component, again like useState
  3. Add ref prop with a pointer to the constant storing the hook initiation

Much like its name implies, a ref creates a reference to a DOM element.

In fact, it returns a DOM element as an object with a current property whose value (aka the ref) is a DOM node.

By using the value property of the return object, you can read data without logging every keystroke (as is the case, so far, with useState).

In this way, a ref enables us to get rid of states. There’s, also, no longer a need to bind by feeding a state back into the input.

Since we’re no longer managing state, a ref component becomes uncontrolled. Its state no longer depends on React because we’re directly manipulating the value of the DOM element.

Tip: Avoid directly manipulating the DOM as its React’s job! Make use of ref when reading values such as those of inputs in forms.

As an example, let’s read the values of the form inputs using ref instead of state.

Form.js

Start by importing the useRef hook: import { useState, Fragment, useRef } from "react"

Replace state initiations and handlers with refs:

const Form = (props) => {
  // let [name, setName] = useState("");
  // let [age, setAge] = useState("");
  let nameRef = useRef(),
      ageRef = useRef();
  let [err, setErr] = useState(false);
  let [errMsg, setErrMsg] = useState();

  // const nameHandler = (e) => {
  //   setName(e.target.value);
  // };

  // const ageHandler = (e) => {
  //   setAge(e.target.value);
  // };
...


At this point, I’m initializing refs for the name and age inputs respectively.

Follow by creating a connection between the initialized ref and the DOM element:

...
<input
          type="text"
          id="name"
          name="name"
          // value={name}
          // onChange={nameHandler}
          ref={nameRef}
        />

        <label htmlFor="age">Age</label>
        <input
          type="number"
          id="age"
          name="age"
          max="100"
          min="1"
          // value={age}
          // onChange={ageHandler}
          ref={ageRef}
        />
...


Notice how we no longer need the value and onChange properties for the inputs.

Now, when submitting the form, access the values of the inputs and check if there’s an error.

const formSubmitHandler = (e) => {
    e.preventDefault();

    let name = nameRef.current.value,
        age = ageRef.current.value;

    // Check if input has error
    if (name.length === 0 || age.length === 0) {
      setErr(true);
      setErrMsg("Please enter a valid name and age (between 1-100).");
      return;
    } else {
      setErrMsg();
    }
...


By storing the ref values in the name and age variables, I avoid having to replace the rest of the error logic.

However, when resetting the values of the inputs after forwarding data to the parent, you can’t simply set name and age to empty strings.

This action will not work and you’ll get an error!

Instead, to reset the input values using refs, directly set the ref values to empty strings.

...
    // Reset input values
    // setName("");
    // setAge("");
    nameRef.current.value = '';
    ageRef.current.value = '';
};


See how everything is working wonderfully?

And, yes, I absolutely took the liberty of making myself young again 😏

Grab the complete project source code on GitHub.

‘Till the next one, be cool 👋

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.

Related Posts