Retour au classement

aidenybai/bippy

TypeScriptbippy.dev

⚠️ hack into react internals

fiberinternalreactreconcilerbippy
Croissance des étoiles
Étoiles
1.3k
Forks
39
Croissance hebdomadaire
Issues
3
5001k
sept. 25déc. 25avr. 26juil. 26
Artefactsnpmnpm install bippy
README

[!WARNING] ⚠️⚠️⚠️ this project may break production apps and cause unexpected behavior ⚠️⚠️⚠️

this project uses react internals, which can change at any time. we don't recommend depending 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

version downloads

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 { instrument, traverseFiber } from "bippy"; // must be imported BEFORE react

instrument({
  onCommitFiberRoot(rendererID, 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 awkward 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 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, react doesn't expose fibers to you directly. so, we have to hack our way around to access them.

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__
    • (instead of directly mutating onCommitFiberRoot, …)
  • traverseRenderedFibers to traverse the fiber tree and determine which fibers have actually rendered
    • (instead of child, sibling, and return pointers)
  • traverseFiber to traverse the fiber tree, regardless of whether it has rendered
    • (instead of child, sibling, and return pointers)
  • setFiberId / getFiberId to set and get a fiber's id
    • (instead of anonymous fibers with no identity)

how to use

we recommend installing via npm.

import this package before your react app runs. it adds a special object to the global scope that react reports its internals to (react devtools uses the same mechanism). as soon as react loads and attaches, bippy starts collecting data about what is going on in react's internals.

npm install bippy

since bippy must load before react, some bundlers need specific configuration to get the import order right.

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: import bippy 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 main bippy export. this only installs the react devtools hook without importing any utility functions, allowing you to import only what you need from bippy/core or define your own fiber utilities. that said, the full bippy package is only ~4 KB 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. import bippy before react, and call instrument before any other methods.

bippy patches each hook event once and dispatches it to a set of listeners, so multiple instrument calls compose instead of replacing each other. instrument returns an unsubscribe function that removes exactly the handlers you registered (also a Disposable, so it works with using).

import { instrument } from "bippy"; // must be imported BEFORE react
import * as React from "react";

const unsubscribe = instrument({
  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);
  },
});

// later, stop listening (other instrument() subscribers keep working)
unsubscribe();

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, traverseRenderedFibers } from "bippy"; // must be imported BEFORE react
import * as React from "react";

instrument({
  onCommitFiberRoot(rendererID, root) {
    traverseRenderedFibers(root, (fiber) => {
      console.log("fiber rendered", fiber);
    });
  },
});

traverseFiber

calls a callback on every fiber in the fiber tree.

import { instrument, traverseFiber } from "bippy"; // must be imported BEFORE react
import * as React from "react";

instrument({
  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

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);
});

traverseContexts

traverses the contexts (useContext) of a fiber.

import { traverseContexts } from "bippy";

// ...

traverseContexts(fiber, (next, prev) => {
  console.log(next, prev);
});

setFiberId / getFiberId

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));

isHostFiber

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");
}

isCompositeFiber

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");
}

getDisplayName

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));

getType

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 <div>hello</div>;
};
const MemoizedComponent = memo(() => {
  return <div>hello</div>;
});

console.log(getType(fiberForMemoizedComponent) === RealComponent);

getNearestHostFiber / getNearestHostFibers

getNearestHostFiber returns the closest host fiber above or below a given fiber. getNearestHostFibers returns all host fibers associated with the provided fiber and its subtree.

import { getNearestHostFiber, getNearestHostFibers } from "bippy";

// ...

function Component() {
  return (
    <>
      <div>hello</div>
      <div>world</div>
    </>
  );
}

console.log(getNearestHostFiber(fiberForComponent)); // <div>hello</div>
console.log(getNearestHostFibers(fiberForComponent)); // [<div>hello</div>, <div>world</div>]

getTimings

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);
}

getFiberStack

returns an array representing the stack of fibers from the current fiber up to the root.

[fiber, fiber.return, fiber.return.return, ...]

getMutatedHostFibers

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));

isValidFiber

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));

getFiberFromHostInstance

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);

getLatestFiber

returns the latest fiber (since it may be double-buffered). usually use this in combination with getFiberFromHostInstance.

import { getLatestFiber } from "bippy";

const latestFiber = getLatestFiber(getFiberFromHostInstance(document.querySelector("div")));
console.log(latestFiber);

overrideProps

overrides component props at runtime by modifying the fiber's props.

import { overrideProps } from "bippy";

// override props on a fiber
overrideProps(fiber, {
  title: "new title",
  config: {
    enabled: true,
    count: 42,
  },
});

the function accepts a fiber and a partial object containing the props to override. bippy automatically flattens nested objects into property paths.

overrideHookState

overrides hook state (useState, useReducer, etc.) at runtime by hook id.

import { overrideHookState } from "bippy";

// override the first hook (id: 0) with a new value
overrideHookState(fiber, 0, "new state value");

// override nested state object
overrideHookState(fiber, 1, {
  user: {
    name: "john",
    age: 30,
  },
});

the hook id parameter corresponds to the order of hooks in the component (0-indexed). pass either a primitive value or an object for nested state updates.

overrideContext

overrides react context values at runtime by finding the appropriate context provider.

import { overrideContext } from "bippy";

// override context value
overrideContext(fiber, MyContext, {
  theme: "dark",
  user: {
    id: 123,
    name: "jane",
  },
});

// override with primitive value
overrideContext(fiber, ThemeContext, "dark");

the function traverses up the fiber tree to find the context provider matching the provided context type and overrides its value.

getSource

gets the source code location of a composite fiber.

import { getSource } from "bippy/source";

// random fiber on the DOM
const hostFiber = getFiberFromHostInstance(document.querySelector("div"));

// get nearest composite fiber up the tree
const compositeFiber = traverseFiber(
  hostFiber,
  (fiber) => {
    if (isCompositeFiber(fiber)) {
      return fiber;
    }
  },
  true,
);

const source = await getSource(compositeFiber);
// {
//   columnNumber: 12,
//   fileName: 'path/to/file.tsx',
//   lineNumber: 12,
// }

caveats:

  • only available in dev mode
  • only works for composite fibers (function/class components)
  • captures the location where the component is used, not where it's defined
  • in react 18, resolves _debugSource directly (see react#31981)
  • in react >18, _debugSource is not available for host fibers

getOwnerStack / getParentStack

returns a symbolicated stack of components above a fiber.

getOwnerStack walks the chain of components that created this fiber's JSX (react's _debugOwner chain), with exact creation-site locations on react 19, including server component owners. wrappers that merely render {children} don't appear. it automatically falls back to getParentStack when no usable owner frames exist (e.g. react <19).

getParentStack walks all ancestors in the render tree (the fiber's return chain), including {children} wrappers. works on every react version.

import { getOwnerStack, getParentStack } from "bippy/source";

const ownerFrames = await getOwnerStack(fiber);
// [{ functionName: "Button", fileName: "src/button.tsx", lineNumber: 12, ... }, ...]

const parentFrames = await getParentStack(fiber);
// includes every wrapper between the fiber and the root

instrumentReactRefresh

subscribes to fast refresh (HMR) updates from bippy/react-refresh. works with any bundler that uses react-refresh (vite, next.js webpack, next.js turbopack, metro) without bundler-specific code: bippy auto-detects the bundler's HMR transport and augments each update with the hot-updated source file paths.

the handler runs after react has re-rendered with the new component types, so updatedFibers/staleFibers are the mounted fibers matching the hot-swapped component types.

returns an unsubscribe function (a no-op during SSR, so no environment checks needed). the returned function is also a Disposable, so it works with using.

import { instrumentReactRefresh } from "bippy/react-refresh";
import { getDisplayName } from "bippy";

const unsubscribe = instrumentReactRefresh({
  onRefresh(update) {
    for (const fiber of update.updatedFibers) {
      console.log("hot updated:", getDisplayName(fiber.type));
    }
    console.log("changed files:", update.filePaths);
  },
});

// later
unsubscribe();

example

here's a mini toy version of react-scan that highlights renders in your app.

import { instrument, getNearestHostFiber, traverseRenderedFibers } from "bippy"; // must be imported BEFORE react

const highlightFiber = (fiber) => {
  if (!(fiber.stateNode instanceof HTMLElement)) return;
  // fiber.stateNode is a DOM element
  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);
};

/**
 * `instrument` is a function that installs the react DevTools global
 * hook and allows you to set up custom handlers for react fiber events.
 */
instrument({
  /**
   * `onCommitFiberRoot` is a handler that is called when react is
   * ready to commit a fiber root. this means that react is has
   * rendered your entire app and is ready to apply changes to
   * the host tree (e.g. via DOM mutations).
   */
  onCommitFiberRoot(rendererID, root) {
    /**
     * `traverseRenderedFibers` traverses the fiber tree and determines which
     * fibers have actually rendered.
     *
     * A fiber tree contains many fibers that may have not rendered. this
     * can be because it bailed out (e.g. `useMemo`) or because it wasn't
     * actually rendered (if <Child> re-rendered, then <Parent> didn't
     * actually render, but exists in the fiber tree).
     */
    traverseRenderedFibers(root, (fiber) => {
      /**
       * `getNearestHostFiber` is a utility function that finds the
       * nearest host fiber to a given fiber.
       *
       * a host fiber for `react-dom` is a fiber that has a DOM element
       * as its `stateNode`.
       */
      const hostFiber = getNearestHostFiber(fiber);
      highlightFiber(hostFiber);
    });
  },
});

glossary

  • fiber: a “unit of execution” in react, representing a component or dom element
  • commit: the process of applying changes to the host tree (e.g. DOM mutations)
  • render: the process of building the fiber tree by executing component function/classes
  • host tree: the tree of UI elements that react mutates (e.g. DOM elements)
  • reconciler (or “renderer”): custom bindings for react, e.g. react-dom, react-native, react-three-fiber, etc to mutate the host tree
  • rendererID: the id of the reconciler, starting at 1 (can be from multiple reconciler instances)
  • root: a special FiberRoot type that contains the container fiber (the one you pass to ReactDOM.createRoot) in the current property
  • onCommitFiberRoot: called when react is ready to commit a fiber root
  • onPostCommitFiberRoot: called when react has committed a fiber root and effects have run
  • onCommitFiberUnmount: called when a fiber unmounts

misc

we initially created bippy for react-scan, which ships with safeguards so it only runs in development or error-guarded in production.

if you're seeking more robust solutions, you might consider its-fine for accessing fibers within react using hooks, or react-devtools-inline for a headful interface.

if you plan to use this project beyond experimentation, please review react-scan's source code to understand our safeguarding practices.

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.

Dépôts similaires
gofiber/fiber

⚡️ Express inspired web framework written in Go

GoGo ModulesMIT Licensegogolang
gofiber.io
40k2k
pmndrs/react-three-fiber

🇨🇭 A React renderer for Three.js

TypeScriptnpmMIT Licensereactthreejs
docs.pmnd.rs/react-three-fiber
31.5k1.9k
7kms/react-illustration-series

图解react源码, 用大量配图的方式, 致力于将react原理表述清楚.

TypeScriptnpmGNU Affero General Public License v3.0reactfiber
7km.top
7.9k836
Bogdan-Lyashenko/Under-the-hood-ReactJS

Entire React code base explanation by visual block schemes (Stack version)

JavaScriptnpmMIT Licensereactjsjavascript
bogdan-lyashenko.github.io/Under-the-hood-ReactJS/
6.8k614
BetaSu/just-react

「React技术揭秘」 一本自顶向下的React源码分析书

JavaScriptnpmMIT Licensereactreactjs
react.iamkasong.com
6.4k553
sylar-yin/sylar

C++高性能分布式服务器框架,webserver,websocket server,自定义tcp_server(包含日志模块,配置模块,线程模块,协程模块,协程调度模块,io协程调度模块,hook模块,socket模块,bytearray序列化,http模块,TcpServer模块,Websocket模块,Https模块等, Smtp邮件模块, MySQL, SQLite3, ORM,Redis,Zookeeper)

C++cppdistribution
sylar.top
4.7k1k
frejs/fre

:ghost: Tiny Concurrent UI library with Fiber.

TypeScriptnpmMIT Licensefrejsx
deepwiki.com/frejs/fre
3.8k348
gofiber/recipes

📁 Examples for 🚀 Fiber

GoGo ModulesMIT Licenseexamplesfiber
docs.gofiber.io/recipes/
3.5k491
fibjs/fibjs

JavaScript on Fiber (built on Chrome's V8 JavaScript engine)

C++Otherjavascriptv8
fibjs.org
3.1k301
acl-dev/acl

C/C++ server and network library, including coroutine,redis client,http/https/websocket,mqtt, mysql/postgresql/sqlite client with C/C++ for Linux, Android, iOS, MacOS, Windows, Harmony,etc..

CGNU Lesser General Public License v2.1coroutinedatabase
acl-dev.cn
3.1k949
zthxxx/react-dev-inspector

jump to local IDE code directly from browser React component by just a simple click

TypeScriptnpmMIT Licensereactfiber
react-dev-inspector.zthxxx.me
1.3k77
pmndrs/its-fine

🐶🔥 A collection of escape hatches for React.

TypeScriptnpmMIT Licensefiberhooks
npmjs.com/its-fine
1.2k15