Escape Hatches - discussion (2022-09-03)
We just did a refactor where we removed a bunch of imperative code and now I'm curious: what escape hatches do you allow by default, and how do you keep them from spreading?
When do you decide an escape hatch is just DOM integration vs a sign the UI contract needs restructuring? Do you wrap escape hatches in named helpers so they are auditable? Do you render a small signal for hatch-driven behavior so it is testable and reproducible?
Comments (10)
Back to latestFocus and measurement, sure. Anything that changes navigation or data posture should be a contract key, not a ref trick.
We allow a small set of hatches, but we require: named helper + a rendered signal when behavior changes.
Related: Accessibility Best Practices and Routing and Navigation.
Wrapping hatches was the big win for us:
tsx
function useScrollToTopOnNav(enabled: boolean) {
useLayoutEffect(() => {
if (!enabled) return;
window.scrollTo({ top: 0, behavior: 'instant' as ScrollBehavior });
}, [enabled]);
}
We make it observable by rendering a tiny marker in the shell when the hatch is enabled (e.g. data-escape='scrollToTop').
Counterpoint: not every hatch deserves a signal.
We only render signals when the hatch affects user-visible behavior that support needs to reason about.
That feels reasonable. The bug that triggered this was focus-related and support couldn't reproduce it reliably.
So for us, focus might be one of the hatch categories where signals are worth it.
Yeah. Focus is one of the few small-but-crucial behaviors that benefits from being test-visible.
I delete hatches that exist to settle state. If you need to settle state, you probably need derived evidence or a different boundary.
We keep an escape hatch inventory by searching for the helper names. That way hatches don't hide as random one-off effects.
We had a performance problem caused by measuring layout on every render. Once we made measurement an explicit hatch with a signal, it was obvious where it was happening and we could throttle it.
tsx
function useMeasureHatch(enabled: boolean, ref: any) {
useLayoutEffect(() => {
if (!enabled) return;
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
// store the evidence once so it doesn't re-measure forever
}, [enabled]);
}
return <div data-escape="measure" data-enabled={String(enabled)} />;Escape hatches are fine. Invisible escape hatches are not.
The biggest issue with hatches is they become tribal knowledge. Naming + signals turns them into something you can reason about.
If you can't explain the hatch from a screenshot, you probably shouldn't ship it.