Bippy: Sneaking Into React's Fiber Tree Like You Own the Place
Every React app secretly hands out a backstage pass, and almost nobody uses it. Before React renders a single component, it looks for a global object called window.__REACT_DEVTOOLS_GLOBAL_HOOK__. That hook is how the React DevTools browser extension listens in on every render commit and walks the internal fiber tree. Bippy is the cheeky little library that impersonates DevTools, installs its own handlers on that hook, and gives you the exact same raw, programmatic access, without changing a line of your component code.
The result is the ability to read props, state, context values, render timing, and source locations from outside React, and even to override them live. It is the engine that powers tools like react-scan, and it weighs about 4kb gzipped with zero runtime dependencies. Created by Aiden Bai (the same mind behind Million.js and react-scan), bippy is unapologetic about what it is: a surgical tool for people building devtools, profilers, and inspectors, not something to casually sprinkle into a production app.
A Word Before You Touch Anything
Let's get the loud part out of the way, because the README practically shouts it at you: this library can break your app. Bippy reaches into undocumented React internals, the shape of the fiber data structure and the DevTools hook, and the React team is free to change those at any time. Fiber fields like memoizedProps and memoizedState have already shifted across React 17, 18, and 19. Patching the hook can also collide with the real DevTools extension or other tools that expect to own it.
So treat bippy as a precision instrument. Wrap your handlers in its secure() helper, gate it to development, and follow the patterns react-scan uses. We'll do exactly that as we go.
What's In The Box
Bippy is small but the surface area is dense with carefully chosen primitives:
- Hook impersonation that feeds you the fiber root on every render commit.
- Fiber traversal helpers to walk the whole tree, or just the fibers that actually re-rendered.
- Inspection utilities for display names, component types, render timings, and stable fiber identity.
- DOM bridging to go from a clicked element back to its component fiber, and vice versa.
- Live override functions for props, hook state, and context, mirroring what DevTools' "edit value" does.
- A
secure()wrapper that try/catches your handlers and validates the React version so a thrown error can't take down the host app. - Multiple entry points, including a roughly 90-byte
bippy/install-hook-onlyfor tooling authors who just need the hook installed early.
Getting It Wired In
Installation is the easy part:
npm install bippy
yarn add bippy
The catch is when it loads. Bippy must run before React boots, so the DevTools hook exists by the time React looks for it. That makes setup framework-specific.
For Next.js 15.3+, create an instrumentation-client.ts file at your app root:
import "bippy";
For Vite, import it at the very top of src/main.tsx, before any React import:
import "bippy";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);
If you're a library author and you only need the hook installed early without pulling the full bundle, reach for the featherweight entry:
import "bippy/install-hook-only";
Listening To Every Heartbeat
The core of Bippy is instrument(), which patches the DevTools hook with your handlers. The most important handler is onCommitFiberRoot, called on every render commit with the root fiber. From a root, you can walk the entire tree with traverseFiber.
import { instrument, traverseFiber } from "bippy";
instrument({
onCommitFiberRoot(rendererID, root) {
traverseFiber(root.current, (fiber) => {
console.log("fiber:", fiber);
});
},
});
A fiber is React's "unit of work." Every fiber is either a composite fiber (a function or class component) or a host fiber (a DOM element like a div). Each one carries runtime metadata: memoizedProps holds current props, memoizedState holds the linked list of hooks, stateNode points at the backing DOM node for host fibers, and child, sibling, and return are the pointers that let you navigate the tree (return is the parent).
This raw version is great for a quick experiment, but for anything real, you want the safety net we'll add next.
Building A Render Highlighter The Safe Way
Here is the canonical Bippy demo: a miniature react-scan that draws a flashing box around every element that re-rendered. Two things make it production-shaped. First, secure() wraps the handlers so a thrown error can't crash the app and it validates the React version. Second, traverseRenderedFibers visits only the fibers that actually re-rendered this commit, not the whole tree, which is exactly how render-tracking tools stay cheap.
import {
instrument,
secure,
getNearestHostFiber,
traverseRenderedFibers,
} from "bippy";
instrument(
secure({
onCommitFiberRoot(rendererID, root) {
traverseRenderedFibers(root, (fiber) => {
const hostFiber = getNearestHostFiber(fiber);
if (!(hostFiber?.stateNode instanceof HTMLElement)) return;
const rect = hostFiber.stateNode.getBoundingClientRect();
const box = document.createElement("div");
Object.assign(box.style, {
border: "1px solid red",
position: "fixed",
top: `${rect.top}px`,
left: `${rect.left}px`,
width: `${rect.width}px`,
height: `${rect.height}px`,
pointerEvents: "none",
});
document.documentElement.appendChild(box);
setTimeout(() => box.remove(), 100);
});
},
})
);
getNearestHostFiber walks down from a composite fiber to find the nearest DOM-backed fiber, since components themselves don't have a DOM node to measure. Make secure() your default habit; the unguarded version above is only for poking around.
Diffing What Changed On Each Render
Once you can see which components re-rendered, the obvious next question is why. Bippy ships dedicated traversal helpers for props, hook state, and context, each handing you the next and previous values so you can diff them. This is the foundation of "why-did-you-render" style tooling, built generically.
import {
instrument,
secure,
traverseRenderedFibers,
traverseProps,
traverseState,
isCompositeFiber,
} from "bippy";
instrument(
secure({
onCommitFiberRoot(_, root) {
traverseRenderedFibers(root, (fiber) => {
if (!isCompositeFiber(fiber)) return;
traverseProps(fiber, (name, next, prev) => {
if (next !== prev) {
console.log("prop changed:", name, prev, "→", next);
}
});
traverseState(fiber, (next, prev) => {
if (next !== prev) {
console.log("state changed:", prev, "→", next);
}
});
});
},
})
);
isCompositeFiber filters out DOM elements so you only inspect real components. There's a matching traverseContexts(fiber, cb) for useContext values too. Add getDisplayName(fiber) to label each log line, and you have a respectable render-reason debugger in a couple dozen lines.
From A Clicked Element To Its Component
One of Bippy's most satisfying tricks is bridging the DOM and the fiber tree in both directions. getFiberFromHostInstance takes a DOM node and hands back the fiber that produced it, which makes "click an element, inspect its component" a one-liner.
import {
getFiberFromHostInstance,
getDisplayName,
getTimings,
} from "bippy";
document.addEventListener("click", (event) => {
const fiber = getFiberFromHostInstance(event.target as HTMLElement);
if (!fiber) return;
console.log("component:", getDisplayName(fiber));
console.log("props:", fiber.memoizedProps);
const { selfTime, totalTime } = getTimings(fiber);
console.log(`render timing — self: ${selfTime}ms, total: ${totalTime}ms`);
});
getTimings returns the self time and total render time for a fiber, which is gold for spotting expensive components. Note that timing data, like getSource, only exists in development builds; production strips it out. This pattern is the seed of a custom in-app inspector that never requires you to expose test or debug hooks in your actual component code.
Rewriting Reality: The Override Functions
Now for the genuinely dangerous part. Bippy can mutate live component state, the same capability behind DevTools' editable values. overrideProps changes a component's props, overrideHookState changes a hook's state by its zero-based index (hook order equals call order in the component), and overrideContext walks up to the provider to override a context value.
import {
getFiberFromHostInstance,
overrideProps,
overrideHookState,
} from "bippy";
const fiber = getFiberFromHostInstance(
document.querySelector("#counter")!
);
if (fiber) {
// Force a prop value
overrideProps(fiber, { label: "Edited live" });
// Override the first useState hook's value
overrideHookState(fiber, 0, 999);
}
These functions are how you'd build a DevTools-style editor panel, or run quick "what if this value were different" experiments. They are also the fastest way to desync React's internal model and corrupt your app's behavior, so reserve them for tooling where you control the blast radius.
Where Bippy Fits
It helps to know the neighbors. its-fine lets you grab the nearest fiber from inside a component via hooks, which is safer and in-tree but scoped to that component; it can't observe the whole tree from outside React the way Bippy does. react-devtools-inline embeds the entire DevTools UI, which is heavyweight and headful when you want the full experience rather than a programmatic API. The official <Profiler> component is supported and safe but coarse, and it requires wrapping your tree.
Bippy is the lowest-level, smallest, zero-dependency, "I'll build my own tooling" option, sitting in the surgical middle ground. The flagship proof that it works responsibly is react-scan, from which Bippy was extracted; it ships the dev-only, error-guarded safeguards the README tells everyone to copy, and it's the best reference for using bippy without setting your app on fire.
The Takeaway
Bippy is a small library with an enormous reach. By impersonating React DevTools and exposing the fiber tree directly, it unlocks render visualizers, why-did-you-render debuggers, in-app inspectors, component-mapping helpers, and live-editing tools, all in about 4kb with no dependencies. The flip side is that every bit of that power rides on undocumented internals that can shift with any React release, which is why the project is still firmly in 0.x and iterating fast.
If you're building tooling for the React ecosystem and you want fiber access from outside your components, bippy is the sharpest knife in the drawer. Just remember the house rules: load it before React, wrap your handlers in secure(), keep the spicy override functions in development, and lean on react-scan as your model for doing it safely. Used with respect, it turns React's hidden plumbing into your playground.