Portals, Fragments & Refs in React

1. 📖 Introduction

Not every React feature is about state or rendering logic — some solve structural problems instead. Fragments let you group elements without adding extra DOM nodes, Portals let you render children into a different part of the DOM tree entirely, and Refs give you a direct escape hatch to imperatively touch DOM nodes or component instances. This tutorial covers all three in depth, including real-world patterns like modals, tooltips, and focus management.

2. 🧩 React Fragments

<Fragment> lets a component return multiple elements without wrapping them in an extra DOM node like a <div>. This keeps your rendered markup clean, especially important for layouts relying on flex or grid from a direct parent.

FragmentBasic.jsx

import { Fragment } from "react";

function UserInfo() {
  return (
    <Fragment>
      <h2>Jane Doe</h2>
      <p>jane@example.com</p>
    </Fragment>
  );
}

3. ✂️ Short Fragment Syntax

The shorthand <></> syntax is equivalent to <Fragment></Fragment>, but more concise — it's the version used throughout most modern React code.

ShortFragment.jsx

function UserInfo() {
  return (
    <>
      <h2>Jane Doe</h2>
      <p>jane@example.com</p>
    </>
  );
}

Caution

The shorthand <> syntax does not accept a key prop — use the full <Fragment> form whenever a key is needed, such as in a mapped list.

4. 🔑 Keyed Fragments

When rendering a list of fragments — each containing multiple sibling elements — the full <Fragment key={...}> syntax is required, since keys can only be applied to the explicit component form.

KeyedFragment.jsx

function Glossary({ terms }) {
  return (
    <dl>
      {terms.map((term) => (
        <Fragment key={term.id}>
          <dt>{term.word}</dt>
          <dd>{term.definition}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

5. 🤔 When to Use Fragments

  • Returning multiple sibling elements from a component without an extra wrapper
  • Preserving valid HTML structure, like table rows or list items, where an extra <div> would be invalid
  • Avoiding unnecessary DOM nesting that could interfere with CSS layout (like flex or grid children)
  • Rendering a keyed group of elements inside a .map() call

6. ❓ What are Portals?

A Portal renders a component's children into a DOM node outside its parent's DOM hierarchy — while keeping it in the same place in the React component tree for context, state, and event purposes.

React Tree (logical)
App
Modal (rendered via Portal)
Actually mounted in document.body, not inside App's DOM

7. 🏗️ Creating Portals

Portals are created with createPortal from react-dom, which takes the JSX to render and a target DOM node.

CreatePortal.jsx

import { createPortal } from "react-dom";

function Modal({ children }) {
  return createPortal(
    <div className="modal-overlay">
      <div className="modal-content">{children}</div>
    </div>,
    document.getElementById("modal-root")
  );
}

Note

The target node — here #modal-root — must already exist in your HTML, typically a sibling of your app's root <div>.

8. 🎯 Portal Rendering

Even though a Portal's DOM output lives elsewhere on the page, it still participates fully in React's normal render cycle — receiving props, using hooks, and re-rendering just like any other component.

PortalRendering.jsx

function App() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="app">
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      {isOpen && (
        <Modal>
          <p>This content renders in #modal-root, not inside .app</p>
        </Modal>
      )}
    </div>
  );
}

9. 🫧 Portal Event Bubbling

Despite rendering into a different DOM location, events dispatched from inside a Portal still bubble up through the React tree as if the Portal were rendered in its logical position — not its physical DOM position.

PortalBubbling.jsx

function App() {
  return (
    <div onClick={() => console.log("Parent div clicked")}>
      <Modal>
        <button onClick={() => console.log("Button clicked")}>Click me</button>
      </Modal>
    </div>
  );
}
// Clicking the button logs both messages, even though Modal's
// DOM node lives outside the parent div in the actual document.

Important

This behavior is what makes Portals safe for things like modals: a click inside the modal can still be caught by an outer click-outside handler, exactly as React developers intuitively expect.

10. 🎭 Common Portal Use Cases

Portals solve a specific visual problem: content that needs to appear above everything else or outside a clipped/overflow container, regardless of where it's logically defined in your component tree.

11. 🪟 Modals

Modals are the textbook Portal use case — they need to render above the entire page, unaffected by any parent's overflow: hidden or z-index stacking context.

ModalExample.jsx

function ConfirmModal({ onConfirm, onCancel }) {
  return createPortal(
    <div className="modal-overlay">
      <div className="modal-box" role="dialog" aria-modal="true">
        <p>Are you sure you want to delete this item?</p>
        <button onClick={onConfirm}>Delete</button>
        <button onClick={onCancel}>Cancel</button>
      </div>
    </div>,
    document.getElementById("modal-root")
  );
}

12. 💬 Dialogs

Dialogs follow the same Portal pattern as modals but are often paired with the native <dialog> element for built-in accessibility features like focus trapping and Escape-to-close behavior.

13. 💡 Tooltips

Tooltips use Portals to escape a parent's clipping context, ensuring the tooltip is never cut off by a container with overflow: hidden.

TooltipExample.jsx

function Tooltip({ text, children }) {
  const [show, setShow] = useState(false);

  return (
    <span onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>
      {children}
      {show &&
        createPortal(
          <div className="tooltip">{text}</div>,
          document.getElementById("tooltip-root")
        )}
    </span>
  );
}

14. 🔽 Dropdown Menus

Similarly to tooltips, dropdown menus rendered via a Portal avoid being visually clipped by a scrollable or overflow-constrained parent container, like a table cell or card.

15. 🍞 Toast Notifications

Toast notifications are almost always rendered at the very top of the DOM, via a single shared Portal target, so they consistently appear above all other page content regardless of which component triggered them.

ToastExample.jsx

function ToastContainer({ toasts }) {
  return createPortal(
    <div className="toast-container">
      {toasts.map((toast) => (
        <div key={toast.id} className="toast">{toast.message}</div>
      ))}
    </div>,
    document.getElementById("toast-root")
  );
}

16. 🪝 useRef

useRef creates a mutable object that persists across renders without triggering a re-render when it changes — commonly used to access DOM nodes directly, or to store any value that shouldn't participate in the render cycle.

UseRefBasic.jsx

function TextInput() {
  const inputRef = useRef(null);

  const focusInput = () => inputRef.current.focus();

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>Focus the input</button>
    </>
  );
}

17. 🏗️ Creating Refs

A ref is created with useRef(initialValue), returning an object of the shape { current: initialValue }. Its current property can be read or mutated freely without causing a re-render.

RefCreation.jsx

const countRef = useRef(0); // { current: 0 }

countRef.current += 1; // mutate directly, no re-render triggered

18. 🎯 Accessing DOM Elements

Passing a ref as the ref prop on a native element (like <input> or <div>) gives you direct access to the underlying DOM node once it mounts.

DomRefAccess.jsx

function VideoPlayer() {
  const videoRef = useRef(null);

  const play = () => videoRef.current.play();

  return (
    <>
      <video ref={videoRef} src="/demo.mp4" />
      <button onClick={play}>Play</button>
    </>
  );
}

19. 🔒 Mutable References

Beyond DOM access, refs are the standard way to store any value that needs to persist across renders but shouldn't cause a re-render when updated — like a timer ID or a previous prop value.

MutableRef.jsx

function Timer() {
  const intervalRef = useRef(null);

  const start = () => {
    intervalRef.current = setInterval(() => console.log("tick"), 1000);
  };

  const stop = () => clearInterval(intervalRef.current);

  return (
    <>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </>
  );
}

20. ➡️ Forwarding Refs

By default, a ref placed on a custom component doesn't automatically reach the underlying DOM node inside it — the component must explicitly forward it.

21. 🔀 forwardRef

forwardRef lets a custom component accept a ref and pass it down to one of its own internal elements, most often a native DOM node.

ForwardRefExample.jsx

const FancyInput = React.forwardRef(function FancyInput(props, ref) {
  return <input ref={ref} className="fancy-input" {...props} />;
});

function Form() {
  const inputRef = useRef(null);
  return <FancyInput ref={inputRef} placeholder="Type here" />;
}

22. 🎛️ useImperativeHandle

useImperativeHandle customizes exactly what a parent sees when it accesses a forwarded ref — exposing a curated set of methods instead of the raw DOM node.

UseImperativeHandle.jsx

const FancyInput = React.forwardRef(function FancyInput(props, ref) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => (inputRef.current.value = ""),
  }));

  return <input ref={inputRef} {...props} />;
});

// Parent can now only call fancyInputRef.current.focus() or .clear()
// -- not access the raw DOM node directly.

Tip

Use useImperativeHandle when you want to expose a small, intentional API to parent components, rather than leaking the full underlying DOM node.

23. 📞 Callback Refs

Instead of useRef, a ref prop can also accept a function, called with the DOM node when it mounts (and with null when it unmounts) — useful when you need to run logic exactly at that moment.

CallbackRef.jsx

function MeasuredBox() {
  const [height, setHeight] = useState(0);

  const measureRef = (node) => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  };

  return <div ref={measureRef}>Content</div>;
}

24. 🖐️ Imperative DOM Manipulation

Refs are the sanctioned "escape hatch" for the rare cases where you need to directly command a DOM node — playing a video, triggering a native animation, or calling an imperative API a third-party library exposes.

Caution

Reach for refs only when React's declarative model genuinely can't express what you need. Using refs to directly mutate content that could be driven by state undermines React's rendering guarantees.

25. 🎯 Focus Management

Refs are commonly used to move keyboard focus programmatically — for example, focusing the first field of a form when it opens, or returning focus to a trigger button after closing a modal.

FocusManagement.jsx

function Modal({ onClose }) {
  const closeButtonRef = useRef(null);

  useEffect(() => {
    closeButtonRef.current?.focus();
  }, []);

  return (
    <div role="dialog" aria-modal="true">
      <button ref={closeButtonRef} onClick={onClose}>Close</button>
    </div>
  );
}

26. 📜 Scroll Management

Refs let you programmatically scroll an element into view or to a specific position, using the native scrollIntoView or scrollTo DOM methods.

ScrollManagement.jsx

function ChatWindow({ messages }) {
  const bottomRef = useRef(null);

  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  return (
    <div>
      {messages.map((m) => <p key={m.id}>{m.text}</p>)}
      <div ref={bottomRef} />
    </div>
  );
}

27. 📏 Measuring DOM Elements

Refs combined with getBoundingClientRect() let you read an element's actual rendered size or position — information that isn't otherwise available from props or state.

MeasureElement.jsx

function useElementWidth() {
  const ref = useRef(null);
  const [width, setWidth] = useState(0);

  useEffect(() => {
    if (ref.current) {
      setWidth(ref.current.getBoundingClientRect().width);
    }
  }, []);

  return [ref, width];
}

28. 🔌 Third-Party Library Integration

Many non-React libraries (chart libraries, map widgets, rich text editors) expect a raw DOM node to initialize themselves against — refs are the standard bridge for wiring these into a React component.

ThirdPartyIntegration.jsx

function ChartWidget({ data }) {
  const containerRef = useRef(null);

  useEffect(() => {
    const chart = new ThirdPartyChart(containerRef.current, { data });
    return () => chart.destroy();
  }, [data]);

  return <div ref={containerRef} />;
}

29. 🏆 Ref Best Practices

  1. Use refs for direct DOM access and imperative actions, not as a substitute for state that should drive rendering
  2. Prefer useImperativeHandle over exposing a raw DOM node when building a reusable component API
  3. Always check a ref is not null before use, since it starts as null before the first render commits
  4. Clean up any imperative side effects (like third-party library instances) tied to a ref inside a useEffect cleanup function

30. ⚠️ Common Mistakes

  • Reading ref.current during rendering, before it has been set by the browser committing the DOM
  • Using a ref to store data that should actually be state, causing the UI not to update when it changes
  • Forgetting to forward a ref through a custom component, so the parent's ref.current stays null
  • Not providing a Portal's target DOM node in HTML before rendering, causing createPortal to throw
  • Assuming Portal content is isolated from event bubbling, when in fact it bubbles through the React tree normally

Danger

Mutating ref.current and expecting the component to re-render is a very common source of confusion — refs are explicitly designed to not trigger renders, unlike state.

31. 💬 Frequently Asked Questions

Do I need a Fragment if a component only returns one element?

No — Fragments are only needed when returning multiple sibling elements. A single root element doesn't require one.

Why use a Portal instead of just placing the modal higher in the component tree?

Moving a component higher in the React tree doesn't change where its output lands in the actual DOM — a parent's overflow: hidden or stacking context can still clip it. Portals solve this at the DOM level while keeping the component logically nested where it makes sense.

Can I use a ref instead of state to trigger a UI update?

No — updating ref.current does not cause a re-render. If a value needs to be reflected in the rendered UI, it belongs in state, not a ref.

32. 📌 Summary

>>Fragments, Portals, and Refs are React's answer to the real world — where markup, layout, and imperative APIs don't always fit neatly into a purely declarative model.