Skip to content

Escape Hatches

Fresh

Some components need to interact with systems outside of React: focus an input, play a video, connect to a chat server. These "escape hatches" let you step outside React when needed.

Referencing Values with Refs

When you want a component to "remember" some information but don't want that information to trigger new renders, use a ref:

jsx
import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return <button onClick={handleClick}>Click me!</button>;
}

Refs vs State

RefsState
useRef(initialValue) returns { current: initialValue }useState(initialValue) returns [value, setValue]
Doesn't trigger re-render when changedTriggers re-render when changed
Mutable: modify ref.current directlyImmutable: use setter function
Don't read/write current during renderingCan read state at any time

Manipulating the DOM with Refs

React automatically updates the DOM, but sometimes you need direct access. Use a ref to get a DOM node:

jsx
import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

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

Scrolling to an Element

jsx
function ScrollToItem() {
  const listRef = useRef(null);

  function scrollToIndex(index) {
    const listNode = listRef.current;
    const imgNode = listNode.querySelectorAll('li > img')[index];
    imgNode.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  }

  return (
    <ul ref={listRef}>
      {items.map((item, i) => (
        <li key={item.id}>
          <img src={item.src} alt={item.alt} />
        </li>
      ))}
    </ul>
  );
}

Ref Callbacks

For dynamic lists where you need refs to multiple elements:

jsx
function getMap() {
  if (!itemsRef.current) {
    itemsRef.current = new Map();
  }
  return itemsRef.current;
}

<li
  key={cat.id}
  ref={(node) => {
    const map = getMap();
    map.set(cat.id, node);
    return () => { map.delete(cat.id); };
  }}
>

Forwarding Refs

By default, React does not let a component access the DOM nodes of another component. A component that wants to expose its DOM node must opt in with forwardRef:

jsx
import { forwardRef, useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

export default function Form() {
  const inputRef = useRef(null);
  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
    </>
  );
}

Synchronizing with Effects

Effects let you run code after rendering. Use them to synchronize with external systems (network, browser APIs, third-party libraries):

jsx
import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  }, [isPlaying]);

  return <video ref={ref} src={src} loop playsInline />;
}

Effect Lifecycle

flowchart TD
    A[Component mounts] --> B[Effect runs]
    B --> C{Dependencies change?}
    C -->|Yes| D[Cleanup runs]
    D --> B
    C -->|No| E[Wait...]
    E --> C
    F[Component unmounts] --> G[Final cleanup runs]

Cleanup Function

Return a cleanup function from your effect:

jsx
useEffect(() => {
  const connection = createConnection(serverUrl, roomId);
  connection.connect();
  return () => connection.disconnect(); // cleanup
}, [serverUrl, roomId]);

You Might Not Need an Effect

Common cases where effects are unnecessary:

  • Transforming data for rendering: compute during render instead
  • Handling user events: use event handlers, not effects
  • Resetting state on prop change: use a key instead
  • Adjusting state on prop change: compute during render
jsx
// BAD: effect to filter a list
const [items, setItems] = useState(allItems);
useEffect(() => {
  setItems(allItems.filter(item => item.category === category));
}, [allItems, category]);

// GOOD: compute during render
const items = allItems.filter(item => item.category === category);

Custom Hooks

Extract reusable logic into custom Hooks:

jsx
import { useState, useEffect } from 'react';

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    function handleOnline() { setIsOnline(true); }
    function handleOffline() { setIsOnline(false); }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOnline;
}

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? 'Online' : 'Disconnected'}</h1>;
}

Custom Hook Rules

  • Names must start with use followed by a capital letter
  • Custom Hooks share stateful logic, not state itself
  • Each call to a Hook gets its own isolated state

When to Extract a Custom Hook

If you find yourself writing the same effect logic in multiple components, extract it into a custom Hook. If a piece of logic could be a standalone function (no hooks inside), it should be a regular function, not a Hook.