[!WARNING] ⚠️⚠️⚠️ this project may break production apps and cause unexpected behavior ⚠️⚠️⚠️
this project uses react internals, which can change at any time. it is not recommended to depend on internals unless you really, really have to. by proceeding, you acknowledge the risk of breaking your own code or apps that use your code.
bippybippy is a toolkit to hack into react internals
by default, you cannot access react internals. bippy bypasses this by "pretending" to be react devtools, giving you access to the fiber tree and other internals.
import { onCommitFiberRoot, traverseFiber } from "bippy"; // must be imported BEFORE react
instrument({
onCommitFiberRoot: (root) => {
traverseFiber(root.current, (fiber) => {
// prints every fiber in the current React tree
console.log("fiber:", fiber);
});
},
});
bippy allows you to access and use react fibers outside of react components.
a react fiber is a "unit of execution." this means react will do something based on the data in a fiber. each fiber either represents a composite (function/class component) or a host (dom element).
here is a live visualization of what the fiber tree looks like, and here is a deep dive article.
fibers are useful because they contain information about the react app (component props, state, contexts, etc.). a simplified version of a fiber looks roughly like this:
interface Fiber {
// component type (function/class)
type: any;
child: Fiber | null;
sibling: Fiber | null;
// stateNode is the host fiber (e.g. DOM element)
stateNode: Node | null;
// parent fiber
return: Fiber | null;
// the previous or current version of the fiber
alternate: Fiber | null;
// saved props input
memoizedProps: any;
// state (useState, useReducer, useSES, etc.)
memoizedState: any;
// contexts (useContext)
dependencies: Dependencies | null;
// effects (useEffect, useLayoutEffect, etc.)
updateQueue: any;
}
here, the child, sibling, and return properties are pointers to other fibers in the tree.
additionally, memoizedProps, memoizedState, and dependencies are the fiber's props, state, and contexts.
while all of the information is there, it's not super easy to work with, and changes frequently across different versions of react. bippy simplifies this by providing utility functions like:
traverseRenderedFibers to detect renders and traverseFiber to traverse the overall fiber treechild, sibling, and return pointers)traverseProps, traverseState, and traverseContexts to traverse the fiber's props, state, and contextsmemoizedProps, memoizedState, and dependencies)however, fibers aren't directly accessible by the user. so, we have to hack our way around to accessing it.
luckily, react reads from a property in the window object: window.__REACT_DEVTOOLS_GLOBAL_HOOK__ and runs handlers on it when certain events happen. this property must exist before react's bundle is executed. this is intended for react devtools, but we can use it to our advantage.
here's what it roughly looks like:
interface __REACT_DEVTOOLS_GLOBAL_HOOK__ {
// list of renderers (react-dom, react-native, etc.)
renderers: Map<RendererID, reactRenderer>;
// called when react has rendered everything for an update and the fiber tree is fully built and ready to
// apply changes to the host tree (e.g. DOM mutations)
onCommitFiberRoot: (rendererID: RendererID, root: FiberRoot, commitPriority?: number) => void;
// called when effects run
onPostCommitFiberRoot: (rendererID: RendererID, root: FiberRoot) => void;
// called when a specific fiber unmounts
onCommitFiberUnmount: (rendererID: RendererID, fiber: Fiber) => void;
}
bippy works by monkey-patching window.__REACT_DEVTOOLS_GLOBAL_HOOK__ with our own custom handlers. bippy simplifies this by providing utility functions like:
instrument to safely patch window.__REACT_DEVTOOLS_GLOBAL_HOOK__onCommitFiberRoot, ...)secure to wrap your handlers in a try/catch and determine if handlers are safe to runwindow.__REACT_DEVTOOLS_GLOBAL_HOOK__ handlers, which may crash your app)traverseRenderedFibers to traverse the fiber tree and determine which fibers have actually renderedchild, sibling, and return pointers)traverseFiber to traverse the fiber tree, regardless of whether it has renderedchild, sibling, and return pointers)setFiberId / getFiberId to set and get a fiber's idwe recommend installing via npm.
this package should be imported before a React app runs. this will add a special object to the global which is used by React for providing its internals to the tool for analysis (React Devtools does the same). as soon as React library is loaded and attached to the tool, bippy starts collecting data about what is going on in React's internals.
npm install bippy
since bippy needs to be imported before react, some bundlers require specific configuration to ensure the correct import order.
in next.js 15.3+, use the instrumentation-client.js file to ensure bippy loads before react. create this file at the root of your application (or inside the src folder if you're using the src directory structure):
// instrumentation-client.ts
import "bippy";
this file executes before react hydration, making it the ideal place to initialize bippy.
in vite, import bippy at the very top of your main entry point (typically src/main.tsx or src/main.ts) before any react imports:
// src/main.tsx
import "bippy";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
// ... rest of your code
the import order is critical: bippy must be imported before any react packages.
note for library maintainers: if you're building a library and want to define your own utility functions while minimizing bundle size, you can use
bippy/install-hook-only(~90 bytes) instead of the mainbippyexport. this only installs the react devtools hook without importing any utility functions, allowing you to import only what you need frombippy/coreor define your own fiber utilities. that said, the fullbippypackage is only ~4kb gzipped, so bundle size is rarely a concern.```typescript import "bippy/install-hook-only"; // only installs the hook import { getRDTHook, traverseFiber } from "bippy/core"; // import only what you need import * as React from "react"; // import react AFTER the hook is installed
const hook = getRDTHook(); // define your own utilities or use only specific ones ```
patches window.__REACT_DEVTOOLS_GLOBAL_HOOK__ with your handlers. must be imported before react, and must be initialized to properly run any other methods.
use with the
securefunction to prevent uncaught errors from crashing your app.
import { instrument, secure } from "bippy"; // must be imported BEFORE react
import * as React from "react";
instrument(
secure({
onCommitFiberRoot(rendererID, root) {
console.log("root ready to commit", root);
},
onPostCommitFiberRoot(rendererID, root) {
console.log("root with effects committed", root);
},
onCommitFiberUnmount(rendererID, fiber) {
console.log("fiber unmounted", fiber);
},
}),
);
returns the window.__REACT_DEVTOOLS_GLOBAL_HOOK__ object. great for advanced use cases, such as accessing or modifying the renderers property.
import { getRDTHook } from "bippy";
const hook = getRDTHook();
console.log(hook);
not every fiber in the fiber tree renders. traverseRenderedFibers allows you to traverse the fiber tree and determine which fibers have actually rendered.
import { instrument, secure, traverseRenderedFibers } from "bippy"; // must be imported BEFORE react
import * as React from "react";
instrument(
secure({
onCommitFiberRoot(rendererID, root) {
traverseRenderedFibers(root, (fiber) => {
console.log("fiber rendered", fiber);
});
},
}),
);
calls a callback on every fiber in the fiber tree.
import { instrument, secure, traverseFiber } from "bippy"; // must be imported BEFORE react
import * as React from "react";
instrument(
secure({
onCommitFiberRoot(rendererID, root) {
traverseFiber(root.current, (fiber) => {
console.log(fiber);
});
},
}),
);
traverses the props of a fiber.
import { traverseProps } from "bippy";
// ...
traverseProps(fiber, (propName, next, prev) => {
console.log(propName, next, prev);
});
traverses the state (useState, useReducer, etc.) and effects that set state of a fiber.
import { traverseState } from "bippy";
// ...
traverseState(fiber, (next, prev) => {
console.log(next, prev);
});
traverses the contexts (useContext) of a fiber.
import { traverseContexts } from "bippy";
// ...
traverseContexts(fiber, (next, prev) => {
console.log(next, prev);
});
set and get a persistent identity for a fiber. by default, fibers are anonymous and have no identity.
import { setFiberId, getFiberId } from "bippy";
// ...
setFiberId(fiber);
console.log("unique id for fiber:", getFiberId(fiber));
returns true if the fiber is a host fiber (e.g., a DOM node in react-dom).
import { isHostFiber } from "bippy";
if (isHostFiber(fiber)) {
console.log("fiber is a host fiber");
}
returns true if the fiber is a composite fiber. composite fibers represent class components, function components, memoized components, and so on (anything that can actually render output).
import { isCompositeFiber } from "bippy";
if (isCompositeFiber(fiber)) {
console.log("fiber is a composite fiber");
}
returns the display name of the fiber's component, falling back to the component's function or class name if available.
import { getDisplayName } from "bippy";
console.log(getDisplayName(fiber));
returns the underlying type (the component definition) for a given fiber. for example, this could be a function component or class component.
import { getType } from "bippy";
import { memo } from "react";
const RealComponent = () => {
return
hello
;
};
const MemoizedComponent = memo(() => {
return
hello
;
});
console.log(getType(fiberForMemoizedComponent) === RealComponent);
getNearestHostFiber returns the closest host fiber above or below a given fiber. getNearestHostFibers(fiber) returns all host fibers associated with the provided fiber and its subtree.
import { getNearestHostFiber, getNearestHostFibers } from "bippy";
// ...
function Component() {
return (
<>
hello
world
</>
);
}
console.log(getNearestHostFiber(fiberForComponent)); //
hello
console.log(getNearestHostFibers(fiberForComponent)); // [
hello
,
world
]
returns the self and total render times for the fiber.
// timings don't exist in react production builds
if (fiber.actualDuration !== undefined) {
const { selfTime, totalTime } = getTimings(fiber);
console.log(selfTime, totalTime);
}
returns an array representing the stack of fibers from the current fiber up to the root.
[fiber, fiber.return, fiber.return.return, ...]
returns an array of all host fibers that have committed and rendered in the provided fiber's subtree.
import { getMutatedHostFibers } from "bippy";
console.log(getMutatedHostFibers(fiber));
returns true if the given object is a valid React Fiber (i.e., has a tag, stateNode, return, child, sibling, etc.).
import { isValidFiber } from "bippy";
console.log(isValidFiber(fiber));
returns the fiber associated with a given host instance (e.g., a DOM element).
import { getFiberFromHostInstance } from "bippy";
const fiber = getFiberFromHostInstance(document.querySelector("div"));
console.log(fiber);
returns the latest fiber (since it may be double-buffered). usually use this in combination with getFiberFromHostInstance.
```typescript import { getLatestFiber } from "bippy";
const latestFiber = getLatestFiber(getFiberFromHostIn
$ claude mcp add bippy \
-- python -m otcore.mcp_server <graph>