bippy logobippy

bippy is a library that allows you to hack into react internals

get startedgithub

⚠️ 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.


before hacking into react, it helps to understand how it works under the hood.

react's core premise is that ui is a pure function of state:

ui = fn(state)

given the same state, you always get the same ui. a component is just a function that takes data and returns a description of what should appear on screen:

function Greeting(name) {
  return { tag: 'h1', props: { children: 'Hello, ' + name } }
}

Greeting('World')
// { tag: 'h1', props: { children: 'Hello, World' } }

complex uis are built by composing these functions together, forming a tree:

function App(user) {
  return {
    tag: 'div',
    props: {
      children: [
        Greeting(user.name),
        Profile(user)
      ]
    }
  }
}

but directly rebuilding the entire dom on every state change would be slow. instead, react uses a virtual dom: a lightweight javascript representation of the ui.

say we have a page showing "Hello World" and the state changes to "Hello React". rather than rebuilding the whole dom, react compares the two virtual trees and figures out only the span text changed:

react then applies just that one change to the real dom. this diffing process is called reconciliation.

in earlier versions, react walked the tree recursively in one synchronous pass. large trees block the main thread and animations drop frames.

but not all updates are equal. a user click should interrupt a background data fetch:

react uses a "pull" approach: instead of applying updates immediately (push), it delays work until necessary. this lets the framework prioritize what matters.

to enable this, react needs to break work into units that can be paused, resumed, or aborted. this is where fiber comes in.

rendering a react app is like calling nested functions. the cpu tracks this with the call stack, and when too much runs at once, the ui freezes.

fiber reimplements the stack as a linked list. each fiber points to its child, sibling, and return (parent):

this structure lets react pause mid-tree, resume later, or abort. this is impossible with a native call stack.

in concrete terms, a fiber is a javascript object:

interface Fiber {
  type: any;
  key: string | null;

  child: Fiber | null;
  sibling: Fiber | null;
  return: Fiber | null;

  alternate: Fiber | null;
  stateNode: Node | null;

  pendingProps: any;
  memoizedProps: any;
  memoizedState: any;

  dependencies: Dependencies | null;
  updateQueue: any;
}

child, sibling, and return form the tree structure as a singly-linked list. alternate links to the corresponding fiber in the other tree (current or work-in-progress). when pendingProps equals memoizedProps, react can skip re-rendering.

react uses double buffering (a technique from game graphics): it maintains a current tree (what's on screen) and builds a work-in-progress tree in the background. when ready, react swaps them in one commit. try clicking the button below to see this in action:

1/12
currentchildchildchildchildsiblingsibling" times""0""Pressed "<button/>Counter()HostRootFiberRoot…workInProgresschildchildchildchildsiblingsibling

fibers aren't directly exposed to users. however, since react 16, react reads from window.__REACT_DEVTOOLS_GLOBAL_HOOK__ and calls handlers on commit. this is what react developer tools uses. the hook must exist before react loads.

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 installs a handler on this hook before react loads. on every commit, react calls the hook and your handler receives the fiber root:

now let's put what we learned into practice. we'll build an inspector that shows you which react component you're hovering over. it's like react devtools, but simpler and yours.

step 1: get the fiber from a dom element

every dom element rendered by react has a fiber attached to it. use getFiberFromHostInstance:

import { getFiberFromHostInstance } from 'bippy';

const fiber = getFiberFromHostInstance(element);

step 2: find the component

dom elements are "host" fibers. use traverseFiber to walk up and find the nearest component:

import {
  traverseFiber,
  isCompositeFiber,
  getDisplayName
} from 'bippy';

const componentFiber = traverseFiber(
  fiber,
  isCompositeFiber,
  true
);
const name = getDisplayName(componentFiber.type);

step 3: highlight it

use getNearestHostFiber to get the dom node from a component fiber, then draw a box around it:

import { getNearestHostFiber } from 'bippy';

const hostFiber = getNearestHostFiber(componentFiber);
const rect = hostFiber.stateNode.getBoundingClientRect();

try it yourself on this page:

that's it! a working react inspector in ~50 lines of code. bippy handles the hard parts: finding fibers, walking the tree, extracting names, and staying compatible across react 16.8 through 19.

why use bippy for this?

without bippy, you'd need to:

bippy is ~3kb and does all of this for you. go build something cool.


if bippy helped you, consider giving it a star on github

star on github

watch this great talk to learn more about react fiber internals, or explore the react codebase

signature