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 open prop instead of calling methods through a ref.

When

The DOM API has no attribute equivalent (showModal(), play(), focus(), third-party widgets).

Use case

A Modal component controlled by an open prop.

Code compare

React
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:

Angular
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: useEffect after render, afterRenderEffect() after Angular updates the view.
  • React's onClose handles the Escape key, which closes the dialog without changing the open prop. The parent must set open back to false, or the modal won't reopen.

Quick reference

ReactAngular
useEffect(() => { ref.current.showModal() }, [open])afterRenderEffect(() => { ... this.open() ... })
useRef for the DOM nodeviewChild()