Bippy
⚠️ hack into react internals
Install / Use
/learn @aidenybai/BippyREADME
[!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.
<img src="https://github.com/aidenybai/bippy/blob/main/.github/public/bippy.png?raw=true" width="60" align="center" /> bippy
bippy 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.
- works outside of react – no react code modification needed
- utility functions that work across modern react (v17-19)
- no prior react source code knowledge required
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);
});
},
});
how it works & motivation
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:
traverseRenderedFibersto detect renders andtraverseFiberto traverse the overall fiber tree- (instead of
child,sibling, andreturnpointers)
- (instead of
traverseProps,traverseState, andtraverseContextsto traverse the fiber's props, state, and contexts- (instead of
memoizedProps,memoizedState, anddependencies)
- (instead of
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:
instrumentto safely patchwindow.__REACT_DEVTOOLS_GLOBAL_HOOK__- (instead of directly mutating
onCommitFiberRoot, ...)
- (instead of directly mutating
secureto wrap your handlers in a try/catch and determine if handlers are safe to run- (instead of rawdogging
window.__REACT_DEVTOOLS_GLOBAL_HOOK__handlers, which may crash your app)
- (instead of rawdogging
traverseRenderedFibersto traverse the fiber tree and determine which fibers have actually rendered- (instead of
child,sibling, andreturnpointers)
- (instead of
traverseFiberto traverse the fiber tree, regardless of whether it has rendered- (instead of
child,sibling, andreturnpointers)
- (instead of
setFiberId/getFiberIdto set and get a fiber's id- (instead of anonymous fibers with no identity)
how to use
we 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.
next.js
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.
vite
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.
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
API reference
instrument
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);
},
}),
);
getRDTHook
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);
traverseRenderedFibers
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);
});
},
}),
);
traverseFiber
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);
});
},
}),
);
traverseProps
traverses the props of a fiber.
import { traverseProps } from "bippy";
// ...
traverseProps(fiber, (propName, next, prev) => {
console.log(propName, next, prev);
});
traverseState
tra
