Escape Hatches - discussion (2022-12-10)
I keep coming back to the idea that every real app ends up needing a few escape hatches, but the hard part is keeping them from becoming the default path for everything that feels difficult in React.
Where do you draw the line between a pragmatic escape hatch and an interop hole that never gets closed? Do you treat imperative DOM reads and writes as a route posture (declared and visible), or as a local component detail? What has helped your team keep escape hatches contained as the app grows?
Comments (10)
Back to latestThe only thing that worked for us was making escape hatches visible in the UI in dev builds.
If you want them contained, you need a contract story for them.
We started treating them like route posture keys (even if the implementation is local), so the behavior is explainable.
Related: Testing and Debugging and Components and Composition.
A concrete pattern that helped: consolidate all imperative reads into one hook and render a tiny evidence marker when it runs.
tsx
function useMeasureOnce(ref: React.RefObject<HTMLElement>) {
const [size, setSize] = React.useState<{ w: number; h: number } | null>(null);
React.useLayoutEffect(() => {
const el = ref.current;
if (!el) return;
const r = el.getBoundingClientRect();
setSize({ w: Math.round(r.width), h: Math.round(r.height) });
}, []);
return size;
}
If it stays centralized, it does not spread like mold through the codebase.
Counterpoint: turning escape hatches into a big governance thing can slow teams down.
Sometimes you just need to bridge a library and move on.
I agree with the spirit. I am not trying to ban them.
The issue is the quiet accumulation: a year later, nobody can tell which imperative bits are load-bearing and which were just expedient.
That is fair. The visible marker idea is a good middle ground.
We use an internal rule: escape hatches are allowed, but they must be wrapped in a component that reads like a real abstraction.
If you cannot name it, you probably should not do it.
One thing the react.dev piece does well is framing: the goal is not purity, it is keeping the model consistent.
If every screen is a snowflake of imperative glue, debugging gets impossible.
A misunderstanding I see a lot: people think escape hatch means put everything in refs.
The safer move is often the opposite: keep the state in the open, and only use the ref to talk to the outside world.
Yes. Refs as a back channel are fine; refs as a shadow store are where things get weird.
We did a simple lint-level convention: any imperative bridge exports a single function that is easy to grep for.
It sounds boring, but it keeps interop from spreading silently.
If you like the forum docs posture: treat escape hatches as a budgeted resource.
You can spend it, but you should be able to answer where you spent it.
Practical heuristic: if the escape hatch needs retries, timeouts, or lifecycle management, it is a route-level concern.
At that point, I want it declared as posture and rendered as evidence somewhere in the shell.