Handling Side Effects & Working with the useEffect() HookEntry 4 of 7
Syncing with DOM and Browser APIs
Using an effect to keep an imperative API in step with a prop or state value.
Notes
Some browser APIs can't be controlled with JSX attributes. The native <dialog> element, for example, only opens as a modal through showModal(). An effect bridges the gap: whenever the open prop changes, call the matching DOM method.
This is the textbook use of useEffect: synchronizing React state with a system outside React. Refs give the effect access to the DOM node (see Refs & Portals, Entry 1).
What / Why / When
- What
Translating declarative props into imperative calls.
- Why
The parent controls the modal with a simple
openprop instead of calling methods through a ref.- When
The DOM API has no attribute equivalent (
showModal(),play(),focus(), third-party widgets).- Use case
A
Modalcomponent controlled by anopenprop.
Code compare
import { useEffect, useRef, type ReactNode } from "react";import { createPortal } from "react-dom";type ModalProps = { open: boolean; onClose: () => void; children: ReactNode;};export function Modal({ open, onClose, children }: ModalProps) { const dialogRef = useRef<HTMLDialogElement>(null); useEffect(() => { if (open) { dialogRef.current?.showModal(); } else { dialogRef.current?.close(); } }, [open]); // the ref isn't a dependency return createPortal( <dialog ref={dialogRef} className="modal" onClose={onClose}> {open ? children : null} </dialog>, document.getElementById("modal-root")!, );}afterRenderEffect() (v19+) runs after the DOM is updated, which makes it the right tool for reading or writing DOM elements:
import { Component, ElementRef, afterRenderEffect, input, output, viewChild } from "@angular/core";@Component({ selector: "app-modal", template: ` <dialog #dialog class="modal" (close)="closed.emit()"> @if (open()) { <ng-content /> } </dialog> `,})export class ModalComponent { open = input(false); closed = output<void>(); private dialog = viewChild.required<ElementRef<HTMLDialogElement>>("dialog"); constructor() { afterRenderEffect(() => { const el = this.dialog().nativeElement; if (this.open()) { el.showModal(); } else { el.close(); } }); }}Differences and gotchas
- Both frameworks run this code after the DOM exists:
useEffectafter render,afterRenderEffect()after Angular updates the view. - React's
onClosehandles the Escape key, which closes the dialog without changing theopenprop. The parent must setopenback tofalse, or the modal won't reopen.
Quick reference
| React | Angular |
|---|---|
useEffect(() => { ref.current.showModal() }, [open]) | afterRenderEffect(() => { ... this.open() ... }) |
useRef for the DOM node | viewChild() |