Working with Refs & PortalsEntry 2 of 6

State vs Refs (and Refs for Non-UI Values)

State and refs both persist values across renders, but only state triggers a re-render when it changes.

Notes

AspectState (useState)Ref (useRef)
Changing it re-executes the componentYesNo
Use forValues directly reflected in the UI"Behind the scenes" values with no direct UI impact, and DOM access
Updated viaSetter functionAssigning .current
ExampleForm values, counters, selected tabTimer IDs, previous values, DOM nodes

A ref is also the right place for any mutable value that must survive re-renders without causing them, such as a timer ID. A plain let variable inside the component resets on every render, and a module-level variable is shared by every instance of the component.

Rule of thumb: use useRef for imperative reads and writes; use useState for reactive data that drives the UI.

What / Why / When

What

Choosing between reactive state and a mutable ref.

Why

Putting non-UI values in state causes wasted renders; putting UI values in refs causes a stale UI.

When

Any time you store a value in a component, ask: "Does the screen need to change when this changes?"

Use case

A countdown challenge that stores its timer ID in a ref and its running status in state.

Code compare

React
import { useRef, useState } from "react";​export function TimerChallenge({ targetTime }: { targetTime: number }) {  const timer = useRef<number | null>(null); // behind the scenes: no re-render needed  const [isRunning, setIsRunning] = useState(false); // shown in the UI​  function handleStart() {    timer.current = window.setTimeout(() => setIsRunning(false), targetTime * 1000);    setIsRunning(true);  }​  function handleStop() {    if (timer.current !== null) clearTimeout(timer.current);    setIsRunning(false);  }​  return (    <button onClick={isRunning ? handleStop : handleStart}>      {isRunning ? "Stop" : "Start"} challenge    </button>  );}

A component class is instantiated once, so a plain field already persists. Only signals are reactive.

Angular
import { Component, DestroyRef, inject, input, signal } from "@angular/core";​@Component({  selector: "app-timer-challenge",  template: `    <button (click)="isRunning() ? stop() : start()">      {{ isRunning() ? "Stop" : "Start" }} challenge    </button>  `,})export class TimerChallengeComponent {  targetTime = input.required<number>();  isRunning = signal(false); // shown in the UI  private timer?: ReturnType<typeof setTimeout>; // plain field: the "ref" equivalent​  constructor() {    inject(DestroyRef).onDestroy(() => clearTimeout(this.timer));  }​  start() {    this.timer = setTimeout(() => this.isRunning.set(false), this.targetTime() * 1000);    this.isRunning.set(true);  }​  stop() {    clearTimeout(this.timer);    this.isRunning.set(false);  }}

Differences and gotchas

  • Angular needs no useRef for non-UI values, because class fields already survive change detection. The React split of "state vs ref" becomes "signal vs plain field" in Angular.
  • Clean up timers when the component is destroyed: useEffect cleanup in React (later chapter), DestroyRef in Angular.

Quick reference

ReactAngular
useState (reactive)signal()
useRef (mutable, non-reactive)Plain class field
ref.current = xthis.field = x