Table of Contents
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.
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 div
s 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:
- If supported by the project set-up, you can use
<> ... </>
(aka “nameless” tags) - Otherwise, import
React
(if you haven’t already done so) into the file and either use<React.Fragment>
or destructureFragment
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.
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.
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:
- 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”div
s offer a good way to keep the code base organized.
- Import
ReactDOM
from the react-dom module to access thecreatePortal
method. Likewise, destructurecreatePortal
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
andonError
properties to ModalOverlay when porting in order to access the props values on the component properly!
To sum things up:
- Store the modal code in a secondary component. We can’t simply store it in a variable because
createPortal
requires a JSX variable. - 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).
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:
- Import the
useRef
hook in the same fashion we importeduseState
- Call
useRef()
inside of the component, again likeuseState
- 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 ref
s:
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 ref
s 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 👋