# Example: Reading density heights reactively

The useDensityHeights recipe from this page run twice — once given a boxed wrapper whose data-density the buttons flip, once given null so it reads the document root — with a readout showing only the scoped one moving.

Source: https://pretable.ai/examples/density-helpers.md

```tsx DensityHeightsDemo.tsx
"use client";

import { useState } from "react";

import { useDensityHeights } from "./useDensityHeights";

type Density = "compact" | "standard" | "spacious";

const DENSITIES: readonly Density[] = ["compact", "standard", "spacious"];

export function DensityHeightsDemo() {
  const [density, setDensity] = useState<Density>("standard");
  const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null);

  // The same hook twice, differing only in the element it is given. The
  // element is the whole of the scoping mechanism: `getDensityHeights` resolves
  // the tokens where that element paints, and the tokens inherit, so the boxed
  // wrapper's own `data-density` is what the first reading sees.
  const scoped = useDensityHeights(wrapper);

  // `null` means "no element", which resolves `document.documentElement` — this
  // docs page's real root, which the buttons below never touch.
  const page = useDensityHeights(null);

  return (
    <div>
      <p style={{ margin: "0 0 8px", fontSize: 13 }}>
        These buttons set <code>data-density</code> on the boxed wrapper below,
        not on <code>&lt;html&gt;</code> — a docs-site adaptation so this demo
        does not re-theme the rest of the page. &quot;Boxed wrapper&quot; is the{" "}
        <code>useDensityHeights</code> recipe above given that element;
        &quot;Page root&quot; is the identical hook given <code>null</code>, so
        it reads the document root and stays constant no matter which button is
        pressed.
      </p>
      <div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
        {DENSITIES.map((option) => (
          <button
            aria-pressed={option === density}
            key={option}
            onClick={() => setDensity(option)}
            type="button"
          >
            {option}
          </button>
        ))}
      </div>
      <div
        data-density={density}
        ref={setWrapper}
        style={{
          background: "var(--pretable-bg-toolbar)",
          borderRadius: 12,
          padding: 16,
        }}
      >
        <table style={{ borderCollapse: "collapse", width: "100%" }}>
          <thead>
            <tr>
              <th style={{ textAlign: "left" }}>Reading</th>
              <th style={{ textAlign: "right" }}>Row height</th>
              <th style={{ textAlign: "right" }}>Header height</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>Boxed wrapper (data-density=&quot;{density}&quot;)</td>
              <td style={{ textAlign: "right" }}>{scoped.rowHeight}px</td>
              <td style={{ textAlign: "right" }}>{scoped.headerHeight}px</td>
            </tr>
            <tr>
              <td>Page root (unaffected)</td>
              <td style={{ textAlign: "right" }}>{page.rowHeight}px</td>
              <td style={{ textAlign: "right" }}>{page.headerHeight}px</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  );
}
```

```ts useDensityHeights.ts
import { useCallback, useRef, useSyncExternalStore } from "react";
import { getDensityHeights, type DensityHeights } from "@pretable/ui";

const SERVER_SNAPSHOT: DensityHeights = { rowHeight: 32, headerHeight: 36 };

// Every element whose attributes could change what `element` resolves to: the
// element itself, then each ancestor up to `<html>`. The density tokens are
// inherited custom properties, so a `data-density` anywhere on that chain is
// what the element paints under — watching the root alone would miss a swap on
// a wrapper, and watching the wrapper alone would miss one on the root.
function scopeChain(element: Element | null): Element[] {
  const chain: Element[] = [];
  for (
    let current = element;
    current !== null;
    current = current.parentElement
  ) {
    chain.push(current);
  }
  const root = document.documentElement;
  if (!chain.includes(root)) chain.push(root);

  return chain;
}

function subscribe(element: Element | null, onChange: () => void): () => void {
  if (typeof document === "undefined") return () => {};

  const observer = new MutationObserver(onChange);

  for (const node of scopeChain(element)) {
    observer.observe(node, {
      attributes: true,
      attributeFilter: ["data-density", "data-theme", "class", "style"],
    });
  }

  return () => observer.disconnect();
}

export function useDensityHeights(element: Element | null): DensityHeights {
  const cached = useRef<DensityHeights | null>(null);

  const subscribeToScope = useCallback(
    (onChange: () => void) => subscribe(element, onChange),
    [element],
  );

  const getSnapshot = useCallback(() => {
    const next = getDensityHeights(element);
    const prev = cached.current;

    // `useSyncExternalStore` calls this on every render and compares by
    // reference. Returning a fresh object each time is an infinite render
    // loop, so hand back the previous one when the numbers have not moved.
    if (
      prev !== null &&
      prev.rowHeight === next.rowHeight &&
      prev.headerHeight === next.headerHeight
    ) {
      return prev;
    }

    cached.current = next;

    return next;
  }, [element]);

  return useSyncExternalStore(
    subscribeToScope,
    getSnapshot,
    () => SERVER_SNAPSHOT,
  );
}
```
