[!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.
bippy

a hacky way to get fibers from react. used internally by react-scan
bippy attempts* to solve two problems:
- it's not possible to write instrumentation for React without the end user changing code
- doing anything useful with fibers requires you to know react source code very well
bippy allows you to access fiber information from outside of react and provides friendly low-level utils for interacting with fibers.
*disclaimer: "attempt" used loosely, i highly recommend not relying on this in production
how it works
bippy allows you to access and use fibers from outside of react.
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 {
type: any;
child: Fiber | null;
sibling: Fiber | null;
stateNode: Node | null;
return: Fiber | null;
memoizedProps: any;
memoizedState: any;
dependencies: Dependencies | null;
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:
createFiberVisitor to detect renders and traverseFiber to traverse the overall fiber tree
- (instead of
child, sibling, and return pointers)
traverseProps, traverseState, and traverseContexts to traverse the fiber's props, state, and contexts
- (instead of
memoizedProps, 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__ {
renderers: Map<RendererID, ReactRenderer>;
onCommitFiberRoot: (
rendererID: RendererID,
root: FiberRoot,
commitPriority?: number
) => void;
onPostCommitFiberRoot: (rendererID: RendererID, root: FiberRoot) => void;
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__
- (instead of directly mutating
onCommitFiberRoot, ...)
secure to 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)
examples
the best way to understand bippy is to read the source code. here are some examples of how you can use it:
a mini react-scan
here's a mini toy version of react-scan that highlights renders in your app.
import {
instrument,
isHostFiber,
getNearestHostFiber,
createFiberVisitor,
} from 'bippy';
const highlightFiber = (fiber) => {
if (!(fiber.stateNode instanceof HTMLElement)) return;
const rect = fiber.stateNode.getBoundingClientRect();
const highlight = document.createElement('div');
highlight.style.border = '1px solid red';
highlight.style.position = 'fixed';
highlight.style.top = `${rect.top}px`;
highlight.style.left = `${rect.left}px`;
highlight.style.width = `${rect.width}px`;
highlight.style.height = `${rect.height}px`;
highlight.style.zIndex = 999999999;
document.documentElement.appendChild(highlight);
setTimeout(() => {
document.documentElement.removeChild(highlight);
}, 100);
};
const visit = createFiberVisitor({
onRender(fiber) {
const hostFiber = getNearestHostFiber(fiber);
highlightFiber(hostFiber);
},
});
instrument(
secure({
onCommitFiberRoot(rendererID, root) {
visit(rendererID, root);
},
})
);
a mini why-did-you-render
here's a mini toy version of why-did-you-render that logs why components re-render.
import {
instrument,
isHostFiber,
createFiberVisitor,
isCompositeFiber,
getDisplayName,
traverseProps,
traverseContexts,
traverseState,
} from 'bippy';
const visit = createFiberVisitor({
onRender(fiber) {
if (!isCompositeFiber(fiber)) return;
const displayName = getDisplayName(fiber);
if (!displayName) return;
const changes = [];
traverseProps(fiber, (propName, next, prev) => {
if (next !== prev) {
changes.push({
name: `prop ${propName}`,
prev,
next,
});
}
});
let contextId = 0;
traverseContexts(fiber, (next, prev) => {
if (next !== prev) {
changes.push({
name: `context ${contextId}`,
prev,
next,
contextId,
});
}
contextId++;
});
let stateId = 0;
traverseState(fiber, (value, prevValue) => {
if (next !== prev) {
changes.push({
name: `state ${stateId}`,
prev,
next,
});
}
stateId++;
});
console.group(
`%c${displayName}`,
'background: hsla(0,0%,70%,.3); border-radius:3px; padding: 0 2px;'
);
for (const { name, prev, next } of changes) {
console.log(`${name}:`, prev, '!==', next);
}
console.groupEnd();
},
});
instrument(
secure({
onCommitFiberRoot(rendererID, root) {
visit(rendererID, root);
},
})
);
misc
the original bippy character is owned and created by @dairyfreerice. this project is not related to the bippy brand, i just think the character is cute.