├── .changeset
├── README.md
└── config.json
├── .github
├── assets
│ └── bippy.png
└── workflows
│ ├── pkg-pr-new.yml
│ └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── biome.json
├── package.json
├── packages
├── bippy
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── scripts
│ │ └── append-banner.ts
│ ├── src
│ │ ├── core.ts
│ │ ├── experiments
│ │ │ └── inspect.tsx
│ │ ├── index.ts
│ │ ├── install-hook-script-string.ts
│ │ ├── jsx-dev-runtime.ts
│ │ ├── jsx-runtime.ts
│ │ ├── rdt-hook.ts
│ │ ├── source.ts
│ │ ├── test
│ │ │ ├── components.tsx
│ │ │ ├── core
│ │ │ │ ├── fiber.test.tsx
│ │ │ │ ├── instrument.test.tsx
│ │ │ │ ├── traversal.test.tsx
│ │ │ │ └── type.test.tsx
│ │ │ └── development
│ │ │ │ ├── multiple-on-active.test.tsx
│ │ │ │ ├── post-react-devtools.test.tsx
│ │ │ │ ├── post-react-refresh.test.tsx
│ │ │ │ ├── post-react.test.tsx
│ │ │ │ ├── pre-react-devtools.test.tsx
│ │ │ │ ├── pre-react-refresh.test.tsx
│ │ │ │ └── pre-react.test.tsx
│ │ └── types.ts
│ ├── tsconfig.json
│ ├── tsdown.config.ts
│ └── vitest.config.ts
├── kitchen-sink
│ ├── index.html
│ ├── package.json
│ ├── public
│ │ ├── bippy.png
│ │ └── thumbnail.png
│ ├── src
│ │ ├── app.tsx
│ │ ├── main.css
│ │ └── main.tsx
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vercel.json
│ └── vite.config.ts
└── next-kitchen-sink
│ ├── .gitignore
│ ├── README.md
│ ├── app
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
│ ├── components
│ └── fiber.tsx
│ ├── next.config.ts
│ ├── package.json
│ ├── postcss.config.mjs
│ ├── public
│ ├── file.svg
│ ├── globe.svg
│ ├── next.svg
│ ├── vercel.svg
│ └── window.svg
│ ├── scripts
│ └── move-dist.ts
│ └── tsconfig.json
├── pnpm-lock.yaml
└── pnpm-workspace.yaml
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json",
3 | "changelog": "@changesets/changelog-git",
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "restricted",
8 | "baseBranch": "main",
9 | "updateInternalDependencies": "patch",
10 | "ignore": ["@bippy/kitchen-sink", "@bippy/next-kitchen-sink"]
11 | }
12 |
--------------------------------------------------------------------------------
/.github/assets/bippy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aidenybai/bippy/c50e3c21cd4599b8cd4a97448c08b9c0e496427d/.github/assets/bippy.png
--------------------------------------------------------------------------------
/.github/workflows/pkg-pr-new.yml:
--------------------------------------------------------------------------------
1 | name: Publish Any Commit
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | build:
6 | runs-on: ubuntu-latest
7 |
8 | steps:
9 | - name: Checkout code
10 | uses: actions/checkout@v4
11 |
12 | - run: npm i -g --force corepack && corepack enable
13 | - uses: actions/setup-node@v4
14 | with:
15 | node-version: 20
16 | cache: 'pnpm'
17 | - name: Install dependencies
18 | run: pnpm install
19 |
20 | - name: Build
21 | run: pnpm build
22 |
23 | - name: Publish
24 | run: pnpx pkg-pr-new publish --compact ./packages/bippy
25 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Run Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - run: npm i -g --force corepack && corepack enable
11 | - uses: actions/checkout@v4
12 | - uses: pnpm/action-setup@v4
13 | - uses: actions/setup-node@v4
14 | with:
15 | node-version: 20
16 | cache: 'pnpm'
17 | - run: pnpm install
18 | - run: pnpm test
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | .env
4 | dist
5 | **/*.tgz
6 | coverage
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2024 Aiden Bai
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > [!WARNING]
2 | > ⚠️⚠️⚠️ **this project may break production apps and cause unexpected behavior** ⚠️⚠️⚠️
3 | >
4 | > 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.
5 |
6 | # bippy
7 |
8 | [](https://bundlephobia.com/package/bippy)
9 | [](https://npmjs.com/package/bippy)
10 | [](https://npmjs.com/package/bippy)
11 |
12 | bippy is a toolkit to **hack into react internals**
13 |
14 | 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.
15 |
16 | - works outside of react – no react code modification needed
17 | - utility functions that work across modern react (v17-19)
18 | - no prior react source code knowledge required
19 |
20 | ```jsx
21 | import { onCommitFiberRoot, traverseFiber } from 'bippy';
22 |
23 | onCommitFiberRoot((root) => {
24 | traverseFiber(root.current, (fiber) => {
25 | // prints every fiber in the current React tree
26 | console.log('fiber:', fiber);
27 | });
28 | });
29 | ```
30 |
31 | ## how it works & motivation
32 |
33 | bippy allows you to **access** and **use** react fibers **outside** of react components.
34 |
35 | 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).
36 |
37 | > here is a [live visualization](https://jser.pro/ddir/rie?reactVersion=18.3.1&snippetKey=hq8jm2ylzb9u8eh468) of what the fiber tree looks like, and here is a [deep dive article](https://jser.dev/2023-07-18-how-react-rerenders/).
38 |
39 | 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:
40 |
41 | ```typescript
42 | interface Fiber {
43 | // component type (function/class)
44 | type: any;
45 |
46 | child: Fiber | null;
47 | sibling: Fiber | null;
48 |
49 | // stateNode is the host fiber (e.g. DOM element)
50 | stateNode: Node | null;
51 |
52 | // parent fiber
53 | return: Fiber | null;
54 |
55 | // the previous or current version of the fiber
56 | alternate: Fiber | null;
57 |
58 | // saved props input
59 | memoizedProps: any;
60 |
61 | // state (useState, useReducer, useSES, etc.)
62 | memoizedState: any;
63 |
64 | // contexts (useContext)
65 | dependencies: Dependencies | null;
66 |
67 | // effects (useEffect, useLayoutEffect, etc.)
68 | updateQueue: any;
69 | }
70 | ```
71 |
72 | here, the `child`, `sibling`, and `return` properties are pointers to other fibers in the tree.
73 |
74 | additionally, `memoizedProps`, `memoizedState`, and `dependencies` are the fiber's props, state, and contexts.
75 |
76 | 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:
77 |
78 | - `traverseRenderedFibers` to detect renders and `traverseFiber` to traverse the overall fiber tree
79 | - _(instead of `child`, `sibling`, and `return` pointers)_
80 | - `traverseProps`, `traverseState`, and `traverseContexts` to traverse the fiber's props, state, and contexts
81 | - _(instead of `memoizedProps`, `memoizedState`, and `dependencies`)_
82 |
83 | however, fibers aren't directly accessible by the user. so, we have to hack our way around to accessing it.
84 |
85 | luckily, react [reads from a property](https://github.com/facebook/react/blob/6a4b46cd70d2672bc4be59dcb5b8dede22ed0cef/packages/react-reconciler/src/reactFiberDevToolsHook.js#L48) 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.
86 |
87 | here's what it roughly looks like:
88 |
89 | ```typescript
90 | interface __REACT_DEVTOOLS_GLOBAL_HOOK__ {
91 | // list of renderers (react-dom, react-native, etc.)
92 | renderers: Map;
93 |
94 | // called when react has rendered everything for an update and the fiber tree is fully built and ready to
95 | // apply changes to the host tree (e.g. DOM mutations)
96 | onCommitFiberRoot: (
97 | rendererID: RendererID,
98 | root: FiberRoot,
99 | commitPriority?: number
100 | ) => void;
101 |
102 | // called when effects run
103 | onPostCommitFiberRoot: (rendererID: RendererID, root: FiberRoot) => void;
104 |
105 | // called when a specific fiber unmounts
106 | onCommitFiberUnmount: (rendererID: RendererID, fiber: Fiber) => void;
107 | }
108 | ```
109 |
110 | bippy works by monkey-patching `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` with our own custom handlers. bippy simplifies this by providing utility functions like:
111 |
112 | - `instrument` to safely patch `window.__REACT_DEVTOOLS_GLOBAL_HOOK__`
113 | - _(instead of directly mutating `onCommitFiberRoot`, ...)_
114 | - `secure` to wrap your handlers in a try/catch and determine if handlers are safe to run
115 | - _(instead of rawdogging `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` handlers, which may crash your app)_
116 | - `traverseRenderedFibers` to traverse the fiber tree and determine which fibers have actually rendered
117 | - _(instead of `child`, `sibling`, and `return` pointers)_
118 | - `traverseFiber` to traverse the fiber tree, regardless of whether it has rendered
119 | - _(instead of `child`, `sibling`, and `return` pointers)_
120 | - `setFiberId` / `getFiberId` to set and get a fiber's id
121 | - _(instead of anonymous fibers with no identity)_
122 |
123 | ## how to use
124 |
125 | you can either install via a npm (recommended) or a script tag.
126 |
127 | 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.
128 |
129 | ```shell
130 | npm install bippy
131 | ```
132 |
133 | or, use via script tag:
134 |
135 | ```html
136 |
137 | ```
138 |
139 | > this will cause bippy to be accessible under a `window.Bippy` global.
140 |
141 | next, you can use the api to get data about the fiber tree. below is a (useful) subset of the api. for the full api, read the [source code](https://github.com/aidenybai/bippy/blob/main/src/core.ts).
142 |
143 | ### onCommitFiberRoot
144 |
145 | a utility function that wraps the `instrument` function and sets the `onCommitFiberRoot` hook.
146 |
147 | ```typescript
148 | import { onCommitFiberRoot } from 'bippy';
149 |
150 | onCommitFiberRoot((root) => {
151 | console.log('root ready to commit', root);
152 | });
153 | ```
154 |
155 | ### instrument
156 |
157 | > the underlying implementation for the `onCommitFiberRoot()` function. this is optional, unless you want to plug into more less common, advanced functionality.
158 |
159 | patches `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` with your handlers. must be imported before react, and must be initialized to properly run any other methods.
160 |
161 | > use with the `secure` function to prevent uncaught errors from crashing your app.
162 |
163 | ```typescript
164 | import { instrument, secure } from 'bippy'; // must be imported BEFORE react
165 | import * as React from 'react';
166 |
167 | instrument(
168 | secure({
169 | onCommitFiberRoot(rendererID, root) {
170 | console.log('root ready to commit', root);
171 | },
172 | onPostCommitFiberRoot(rendererID, root) {
173 | console.log('root with effects committed', root);
174 | },
175 | onCommitFiberUnmount(rendererID, fiber) {
176 | console.log('fiber unmounted', fiber);
177 | },
178 | })
179 | );
180 | ```
181 |
182 | ### getRDTHook
183 |
184 | returns the `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` object. great for advanced use cases, such as accessing or modifying the `renderers` property.
185 |
186 | ```typescript
187 | import { getRDTHook } from 'bippy';
188 |
189 | const hook = getRDTHook();
190 | console.log(hook);
191 | ```
192 |
193 | ### traverseRenderedFibers
194 |
195 | not every fiber in the fiber tree renders. `traverseRenderedFibers` allows you to traverse the fiber tree and determine which fibers have actually rendered.
196 |
197 | ```typescript
198 | import { instrument, secure, traverseRenderedFibers } from 'bippy'; // must be imported BEFORE react
199 | import * as React from 'react';
200 |
201 | instrument(
202 | secure({
203 | onCommitFiberRoot(rendererID, root) {
204 | traverseRenderedFibers(root, (fiber) => {
205 | console.log('fiber rendered', fiber);
206 | });
207 | },
208 | })
209 | );
210 | ```
211 |
212 | ### traverseFiber
213 |
214 | calls a callback on every fiber in the fiber tree.
215 |
216 | ```typescript
217 | import { instrument, secure, traverseFiber } from 'bippy'; // must be imported BEFORE react
218 | import * as React from 'react';
219 |
220 | instrument(
221 | secure({
222 | onCommitFiberRoot(rendererID, root) {
223 | traverseFiber(root.current, (fiber) => {
224 | console.log(fiber);
225 | });
226 | },
227 | })
228 | );
229 | ```
230 |
231 | ### traverseProps
232 |
233 | traverses the props of a fiber.
234 |
235 | ```typescript
236 | import { traverseProps } from 'bippy';
237 |
238 | // ...
239 |
240 | traverseProps(fiber, (propName, next, prev) => {
241 | console.log(propName, next, prev);
242 | });
243 | ```
244 |
245 | ### traverseState
246 |
247 | traverses the state (useState, useReducer, etc.) and effects that set state of a fiber.
248 |
249 | ```typescript
250 | import { traverseState } from 'bippy';
251 |
252 | // ...
253 |
254 | traverseState(fiber, (next, prev) => {
255 | console.log(next, prev);
256 | });
257 | ```
258 |
259 | ### traverseContexts
260 |
261 | traverses the contexts (useContext) of a fiber.
262 |
263 | ```typescript
264 | import { traverseContexts } from 'bippy';
265 |
266 | // ...
267 |
268 | traverseContexts(fiber, (next, prev) => {
269 | console.log(next, prev);
270 | });
271 | ```
272 |
273 | ### setFiberId / getFiberId
274 |
275 | set and get a persistent identity for a fiber. by default, fibers are anonymous and have no identity.
276 |
277 | ```typescript
278 | import { setFiberId, getFiberId } from 'bippy';
279 |
280 | // ...
281 |
282 | setFiberId(fiber);
283 | console.log('unique id for fiber:', getFiberId(fiber));
284 | ```
285 |
286 | ### isHostFiber
287 |
288 | returns `true` if the fiber is a host fiber (e.g., a DOM node in react-dom).
289 |
290 | ```typescript
291 | import { isHostFiber } from 'bippy';
292 |
293 | if (isHostFiber(fiber)) {
294 | console.log('fiber is a host fiber');
295 | }
296 | ```
297 |
298 | ### isCompositeFiber
299 |
300 | 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).
301 |
302 | ```typescript
303 | import { isCompositeFiber } from 'bippy';
304 |
305 | if (isCompositeFiber(fiber)) {
306 | console.log('fiber is a composite fiber');
307 | }
308 | ```
309 |
310 | ### getDisplayName
311 |
312 | returns the display name of the fiber's component, falling back to the component's function or class name if available.
313 |
314 | ```typescript
315 | import { getDisplayName } from 'bippy';
316 |
317 | console.log(getDisplayName(fiber));
318 | ```
319 |
320 | ### getType
321 |
322 | returns the underlying type (the component definition) for a given fiber. for example, this could be a function component or class component.
323 |
324 | ```jsx
325 | import { getType } from 'bippy';
326 | import { memo } from 'react';
327 |
328 | const RealComponent = () => {
329 | return
]
358 | ```
359 |
360 | ### getTimings
361 |
362 | returns the self and total render times for the fiber.
363 |
364 | ```typescript
365 | // timings don't exist in react production builds
366 | if (fiber.actualDuration !== undefined) {
367 | const { selfTime, totalTime } = getTimings(fiber);
368 | console.log(selfTime, totalTime);
369 | }
370 | ```
371 |
372 | ### getFiberStack
373 |
374 | returns an array representing the stack of fibers from the current fiber up to the root.
375 |
376 | ```typescript
377 | [fiber, fiber.return, fiber.return.return, ...]
378 | ```
379 |
380 | ### getMutatedHostFibers
381 |
382 | returns an array of all host fibers that have committed and rendered in the provided fiber's subtree.
383 |
384 | ```typescript
385 | import { getMutatedHostFibers } from 'bippy';
386 |
387 | console.log(getMutatedHostFibers(fiber));
388 | ```
389 |
390 | ### isValidFiber
391 |
392 | returns `true` if the given object is a valid React Fiber (i.e., has a tag, stateNode, return, child, sibling, etc.).
393 |
394 | ```typescript
395 | import { isValidFiber } from 'bippy';
396 |
397 | console.log(isValidFiber(fiber));
398 | ```
399 |
400 | ## getFiberFromHostInstance
401 |
402 | returns the fiber associated with a given host instance (e.g., a DOM element).
403 |
404 | ```typescript
405 | import { getFiberFromHostInstance } from 'bippy';
406 |
407 | const fiber = getFiberFromHostInstance(document.querySelector('div'));
408 | console.log(fiber);
409 | ```
410 |
411 | ## getLatestFiber
412 |
413 | returns the latest fiber (since it may be double-buffered). usually use this in combination with `getFiberFromHostInstance`.
414 |
415 | ```typescript
416 | import { getLatestFiber } from 'bippy';
417 |
418 | const latestFiber = getLatestFiber(
419 | getFiberFromHostInstance(document.querySelector('div'))
420 | );
421 | console.log(latestFiber);
422 | ```
423 |
424 | ## examples
425 |
426 | the best way to understand bippy is to [read the source code](https://github.com/aidenybai/bippy/blob/main/src/core.ts). here are some examples of how you can use it:
427 |
428 | ### a mini react-scan
429 |
430 | here's a mini toy version of [`react-scan`](https://github.com/aidenybai/react-scan) that highlights renders in your app.
431 |
432 | ```javascript
433 | import {
434 | instrument,
435 | isHostFiber,
436 | getNearestHostFiber,
437 | traverseRenderedFibers,
438 | } from 'bippy'; // must be imported BEFORE react
439 |
440 | const highlightFiber = (fiber) => {
441 | if (!(fiber.stateNode instanceof HTMLElement)) return;
442 | // fiber.stateNode is a DOM element
443 | const rect = fiber.stateNode.getBoundingClientRect();
444 | const highlight = document.createElement('div');
445 | highlight.style.border = '1px solid red';
446 | highlight.style.position = 'fixed';
447 | highlight.style.top = `${rect.top}px`;
448 | highlight.style.left = `${rect.left}px`;
449 | highlight.style.width = `${rect.width}px`;
450 | highlight.style.height = `${rect.height}px`;
451 | highlight.style.zIndex = 999999999;
452 | document.documentElement.appendChild(highlight);
453 | setTimeout(() => {
454 | document.documentElement.removeChild(highlight);
455 | }, 100);
456 | };
457 |
458 | /**
459 | * `instrument` is a function that installs the react DevTools global
460 | * hook and allows you to set up custom handlers for react fiber events.
461 | */
462 | instrument(
463 | /**
464 | * `secure` is a function that wraps your handlers in a try/catch
465 | * and prevents it from crashing the app. it also prevents it from
466 | * running on unsupported react versions and during production.
467 | *
468 | * this is not required but highly recommended to provide "safeguards"
469 | * in case something breaks.
470 | */
471 | secure({
472 | /**
473 | * `onCommitFiberRoot` is a handler that is called when react is
474 | * ready to commit a fiber root. this means that react is has
475 | * rendered your entire app and is ready to apply changes to
476 | * the host tree (e.g. via DOM mutations).
477 | */
478 | onCommitFiberRoot(rendererID, root) {
479 | /**
480 | * `traverseRenderedFibers` traverses the fiber tree and determines which
481 | * fibers have actually rendered.
482 | *
483 | * A fiber tree contains many fibers that may have not rendered. this
484 | * can be because it bailed out (e.g. `useMemo`) or because it wasn't
485 | * actually rendered (if re-rendered, then didn't
486 | * actually render, but exists in the fiber tree).
487 | */
488 | traverseRenderedFibers(root, (fiber) => {
489 | /**
490 | * `getNearestHostFiber` is a utility function that finds the
491 | * nearest host fiber to a given fiber.
492 | *
493 | * a host fiber for `react-dom` is a fiber that has a DOM element
494 | * as its `stateNode`.
495 | */
496 | const hostFiber = getNearestHostFiber(fiber);
497 | highlightFiber(hostFiber);
498 | });
499 | },
500 | })
501 | );
502 | ```
503 |
504 | ### a mini why-did-you-render
505 |
506 | here's a mini toy version of [`why-did-you-render`](https://github.com/welldone-software/why-did-you-render) that logs why components re-render.
507 |
508 | ```typescript
509 | import {
510 | instrument,
511 | isHostFiber,
512 | traverseRenderedFibers,
513 | isCompositeFiber,
514 | getDisplayName,
515 | traverseProps,
516 | traverseContexts,
517 | traverseState,
518 | } from 'bippy'; // must be imported BEFORE react
519 |
520 | instrument(
521 | secure({
522 | onCommitFiberRoot(rendererID, root) {
523 | traverseRenderedFibers(root, (fiber) => {
524 | /**
525 | * `isCompositeFiber` is a utility function that checks if a fiber is a composite fiber.
526 | * a composite fiber is a fiber that represents a function or class component.
527 | */
528 | if (!isCompositeFiber(fiber)) return;
529 |
530 | /**
531 | * `getDisplayName` is a utility function that gets the display name of a fiber.
532 | */
533 | const displayName = getDisplayName(fiber);
534 | if (!displayName) return;
535 |
536 | const changes = [];
537 |
538 | /**
539 | * `traverseProps` is a utility function that traverses the props of a fiber.
540 | */
541 | traverseProps(fiber, (propName, next, prev) => {
542 | if (next !== prev) {
543 | changes.push({
544 | name: `prop ${propName}`,
545 | prev,
546 | next,
547 | });
548 | }
549 | });
550 |
551 | let contextId = 0;
552 | /**
553 | * `traverseContexts` is a utility function that traverses the contexts of a fiber.
554 | * Contexts don't have a "name" like props, so we use an id to identify them.
555 | */
556 | traverseContexts(fiber, (next, prev) => {
557 | if (next !== prev) {
558 | changes.push({
559 | name: `context ${contextId}`,
560 | prev,
561 | next,
562 | contextId,
563 | });
564 | }
565 | contextId++;
566 | });
567 |
568 | let stateId = 0;
569 | /**
570 | * `traverseState` is a utility function that traverses the state of a fiber.
571 | *
572 | * State don't have a "name" like props, so we use an id to identify them.
573 | */
574 | traverseState(fiber, (value, prevValue) => {
575 | if (next !== prev) {
576 | changes.push({
577 | name: `state ${stateId}`,
578 | prev,
579 | next,
580 | });
581 | }
582 | stateId++;
583 | });
584 |
585 | console.group(
586 | `%c${displayName}`,
587 | 'background: hsla(0,0%,70%,.3); border-radius:3px; padding: 0 2px;'
588 | );
589 | for (const { name, prev, next } of changes) {
590 | console.log(`${name}:`, prev, '!==', next);
591 | }
592 | console.groupEnd();
593 | });
594 | },
595 | })
596 | );
597 | ```
598 |
599 | ## glossary
600 |
601 | - fiber: a "unit of execution" in react, representing a component or dom element
602 | - commit: the process of applying changes to the host tree (e.g. DOM mutations)
603 | - render: the process of building the fiber tree by executing component function/classes
604 | - host tree: the tree of UI elements that react mutates (e.g. DOM elements)
605 | - reconciler (or "renderer"): custom bindings for react, e.g. react-dom, react-native, react-three-fiber, etc to mutate the host tree
606 | - `rendererID`: the id of the reconciler, starting at 1 (can be from multiple reconciler instances)
607 | - `root`: a special `FiberRoot` type that contains the container fiber (the one you pass to `ReactDOM.createRoot`) in the `current` property
608 | - `onCommitFiberRoot`: called when react is ready to commit a fiber root
609 | - `onPostCommitFiberRoot`: called when react has committed a fiber root and effects have run
610 | - `onCommitFiberUnmount`: called when a fiber unmounts
611 |
612 | ## development
613 |
614 | pre-requisite: you should understand how react works internally. if you don't, please give this [series of articles](https://jser.dev/series/react-source-code-walkthrough) a read.
615 |
616 | we use a pnpm monorepo, get started by running:
617 |
618 | ```shell
619 | pnpm install
620 | # create dev builds
621 | pnpm run dev
622 | # run unit tests
623 | pnpm run test
624 | ```
625 |
626 | you can ad-hoc test by running `pnpm run dev` in the `/kitchen-sink` directory.
627 |
628 | ```shell
629 | cd kitchen-sink
630 | pnpm run dev
631 | ```
632 |
633 | ## misc
634 |
635 | we use this project internally in [react-scan](https://github.com/aidenybai/react-scan), which is deployed with proper safeguards to ensure it's only used in development or error-guarded in production.
636 |
637 | while i maintain this specifically for react-scan, those seeking more robust solutions might consider [its-fine](https://github.com/pmndrs/its-fine) for accessing fibers within react using hooks, or [react-devtools-inline](https://www.npmjs.com/package/react-devtools-inline) for a headful interface.
638 |
639 | if you plan to use this project beyond experimentation, please review [react-scan's source code](https://github.com/aidenybai/react-scan) to understand our safeguarding practices.
640 |
641 | the original bippy character is owned and created by [@dairyfreerice](https://www.instagram.com/dairyfreerice). this project is not related to the bippy brand, i just think the character is cute.
642 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3 | "vcs": {
4 | "enabled": false,
5 | "clientKind": "git",
6 | "useIgnoreFile": false
7 | },
8 | "files": {
9 | "ignoreUnknown": false,
10 | "include": ["packages/**/*.ts", "packages/**/*.tsx", "*.ts", "*.tsx"],
11 | "ignore": ["node_modules", "dist", "coverage", "css-to-tailwind.ts"]
12 | },
13 | "formatter": {
14 | "enabled": true,
15 | "indentStyle": "space",
16 | "indentWidth": 2,
17 | "lineWidth": 80,
18 | "lineEnding": "lf"
19 | },
20 | "javascript": {
21 | "formatter": {
22 | "quoteStyle": "single",
23 | "trailingCommas": "all"
24 | }
25 | },
26 | "organizeImports": {
27 | "enabled": true
28 | },
29 | "linter": {
30 | "enabled": true,
31 | "rules": {
32 | "recommended": true,
33 | "suspicious": {
34 | "noConsoleLog": {
35 | "level": "warn",
36 | "fix": "unsafe"
37 | }
38 | },
39 | "correctness": {
40 | "noUnusedFunctionParameters": {
41 | "level": "warn",
42 | "fix": "unsafe"
43 | },
44 | "noUnusedImports": {
45 | "level": "warn",
46 | "fix": "unsafe"
47 | },
48 | "noUnusedLabels": {
49 | "level": "warn",
50 | "fix": "unsafe"
51 | },
52 | "noUnusedPrivateClassMembers": {
53 | "level": "warn",
54 | "fix": "unsafe"
55 | },
56 | "noUnusedVariables": {
57 | "level": "warn",
58 | "fix": "unsafe"
59 | }
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bippy/monorepo",
3 | "scripts": {
4 | "dev": "pnpm --filter=./packages/bippy run dev",
5 | "build": "pnpm --filter=./packages/bippy run build",
6 | "test": "pnpm --filter=./packages/bippy run test",
7 | "publint": "pnpm --filter=./packages/bippy run publint",
8 | "lint": "pnpm biome lint --write",
9 | "sherif": "sherif --fix",
10 | "format": "pnpm biome format --write",
11 | "check": "pnpm biome check --write",
12 | "bump": "changeset && changeset version",
13 | "release": "changeset publish"
14 | },
15 | "packageManager": "pnpm@10.11.0",
16 | "private": true,
17 | "devDependencies": {
18 | "@biomejs/biome": "1.9.4",
19 | "@changesets/changelog-git": "^0.2.0",
20 | "@changesets/cli": "^2.27.11",
21 | "sherif": "^1.2.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/bippy/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # bippy
2 |
3 | ## 0.3.15
4 |
5 | ### Patch Changes
6 |
7 | - fix \_source
8 |
9 | ## 0.3.14
10 |
11 | ### Patch Changes
12 |
13 | - tsdown
14 |
15 | ## 0.3.13
16 |
17 | ### Patch Changes
18 |
19 | - remove file protocol
20 |
21 | ## 0.3.12
22 |
23 | ### Patch Changes
24 |
25 | - fix: getFiberSource on next.js
26 |
27 | ## 0.3.11
28 |
29 | ### Patch Changes
30 |
31 | - remove react bundle from bippy/source
32 |
33 | ## 0.3.10
34 |
35 | ### Patch Changes
36 |
37 | - upgrade getFiberSource
38 |
39 | ## 0.3.9
40 |
41 | ### Patch Changes
42 |
43 | - fix getLatestFiber bug
44 |
45 | ## 0.3.7
46 |
47 | ### Patch Changes
48 |
49 | - fix inspect mounting
50 |
51 | ## 0.3.6
52 |
53 | ### Patch Changes
54 |
55 | - fix side effects
56 |
57 | ## 0.3.5
58 |
59 | ### Patch Changes
60 |
61 | - fix instrument
62 |
63 | ## 0.3.4
64 |
65 | ### Patch Changes
66 |
67 | - fix inspect
68 |
69 | ## 0.3.3
70 |
71 | ### Patch Changes
72 |
73 | - remove log in inspect
74 |
75 | ## 0.3.2
76 |
77 | ### Patch Changes
78 |
79 | - fix #27
80 |
81 | ## 0.3.1
82 |
83 | ### Patch Changes
84 |
85 | - fix source
86 |
87 | ## 0.3.0
88 |
89 | ### Minor Changes
90 |
91 | - adds getFiberSource for bippy/source
92 |
93 | ## 0.2.24
94 |
95 | ### Patch Changes
96 |
97 | - fix devtools fail
98 |
99 | ## 0.2.23
100 |
101 | ### Patch Changes
102 |
103 | - fix onActive listeners
104 |
105 | ## 0.2.22
106 |
107 | ### Patch Changes
108 |
109 | - Fix readme (again 2)
110 |
111 | ## 0.2.21
112 |
113 | ### Patch Changes
114 |
115 | - ebfdac3: Fix README (again)
116 | - 1dcefaa: Fix README
117 |
118 | ## 0.2.20
119 |
120 | ### Patch Changes
121 |
122 | - 5af368d: Add tests
123 |
--------------------------------------------------------------------------------
/packages/bippy/README.md:
--------------------------------------------------------------------------------
1 | > [!WARNING]
2 | > ⚠️⚠️⚠️ **this project may break production apps and cause unexpected behavior** ⚠️⚠️⚠️
3 | >
4 | > 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.
5 |
6 | # bippy
7 |
8 | [](https://bundlephobia.com/package/bippy)
9 | [](https://npmjs.com/package/bippy)
10 | [](https://npmjs.com/package/bippy)
11 |
12 | bippy is a toolkit to **hack into react internals**
13 |
14 | 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.
15 |
16 | - works outside of react – no react code modification needed
17 | - utility functions that work across modern react (v17-19)
18 | - no prior react source code knowledge required
19 |
20 | ```jsx
21 | import { onCommitFiberRoot, traverseFiber } from 'bippy';
22 |
23 | onCommitFiberRoot((root) => {
24 | traverseFiber(root.current, (fiber) => {
25 | // prints every fiber in the current React tree
26 | console.log('fiber:', fiber);
27 | });
28 | });
29 | ```
30 |
31 | ## how it works & motivation
32 |
33 | bippy allows you to **access** and **use** react fibers **outside** of react components.
34 |
35 | 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).
36 |
37 | > here is a [live visualization](https://jser.pro/ddir/rie?reactVersion=18.3.1&snippetKey=hq8jm2ylzb9u8eh468) of what the fiber tree looks like, and here is a [deep dive article](https://jser.dev/2023-07-18-how-react-rerenders/).
38 |
39 | 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:
40 |
41 | ```typescript
42 | interface Fiber {
43 | // component type (function/class)
44 | type: any;
45 |
46 | child: Fiber | null;
47 | sibling: Fiber | null;
48 |
49 | // stateNode is the host fiber (e.g. DOM element)
50 | stateNode: Node | null;
51 |
52 | // parent fiber
53 | return: Fiber | null;
54 |
55 | // the previous or current version of the fiber
56 | alternate: Fiber | null;
57 |
58 | // saved props input
59 | memoizedProps: any;
60 |
61 | // state (useState, useReducer, useSES, etc.)
62 | memoizedState: any;
63 |
64 | // contexts (useContext)
65 | dependencies: Dependencies | null;
66 |
67 | // effects (useEffect, useLayoutEffect, etc.)
68 | updateQueue: any;
69 | }
70 | ```
71 |
72 | here, the `child`, `sibling`, and `return` properties are pointers to other fibers in the tree.
73 |
74 | additionally, `memoizedProps`, `memoizedState`, and `dependencies` are the fiber's props, state, and contexts.
75 |
76 | 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:
77 |
78 | - `traverseRenderedFibers` to detect renders and `traverseFiber` to traverse the overall fiber tree
79 | - _(instead of `child`, `sibling`, and `return` pointers)_
80 | - `traverseProps`, `traverseState`, and `traverseContexts` to traverse the fiber's props, state, and contexts
81 | - _(instead of `memoizedProps`, `memoizedState`, and `dependencies`)_
82 |
83 | however, fibers aren't directly accessible by the user. so, we have to hack our way around to accessing it.
84 |
85 | luckily, react [reads from a property](https://github.com/facebook/react/blob/6a4b46cd70d2672bc4be59dcb5b8dede22ed0cef/packages/react-reconciler/src/reactFiberDevToolsHook.js#L48) 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.
86 |
87 | here's what it roughly looks like:
88 |
89 | ```typescript
90 | interface __REACT_DEVTOOLS_GLOBAL_HOOK__ {
91 | // list of renderers (react-dom, react-native, etc.)
92 | renderers: Map;
93 |
94 | // called when react has rendered everything for an update and the fiber tree is fully built and ready to
95 | // apply changes to the host tree (e.g. DOM mutations)
96 | onCommitFiberRoot: (
97 | rendererID: RendererID,
98 | root: FiberRoot,
99 | commitPriority?: number
100 | ) => void;
101 |
102 | // called when effects run
103 | onPostCommitFiberRoot: (rendererID: RendererID, root: FiberRoot) => void;
104 |
105 | // called when a specific fiber unmounts
106 | onCommitFiberUnmount: (rendererID: RendererID, fiber: Fiber) => void;
107 | }
108 | ```
109 |
110 | bippy works by monkey-patching `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` with our own custom handlers. bippy simplifies this by providing utility functions like:
111 |
112 | - `instrument` to safely patch `window.__REACT_DEVTOOLS_GLOBAL_HOOK__`
113 | - _(instead of directly mutating `onCommitFiberRoot`, ...)_
114 | - `secure` to wrap your handlers in a try/catch and determine if handlers are safe to run
115 | - _(instead of rawdogging `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` handlers, which may crash your app)_
116 | - `traverseRenderedFibers` to traverse the fiber tree and determine which fibers have actually rendered
117 | - _(instead of `child`, `sibling`, and `return` pointers)_
118 | - `traverseFiber` to traverse the fiber tree, regardless of whether it has rendered
119 | - _(instead of `child`, `sibling`, and `return` pointers)_
120 | - `setFiberId` / `getFiberId` to set and get a fiber's id
121 | - _(instead of anonymous fibers with no identity)_
122 |
123 | ## how to use
124 |
125 | you can either install via a npm (recommended) or a script tag.
126 |
127 | 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.
128 |
129 | ```shell
130 | npm install bippy
131 | ```
132 |
133 | or, use via script tag:
134 |
135 | ```html
136 |
137 | ```
138 |
139 | > this will cause bippy to be accessible under a `window.Bippy` global.
140 |
141 | next, you can use the api to get data about the fiber tree. below is a (useful) subset of the api. for the full api, read the [source code](https://github.com/aidenybai/bippy/blob/main/src/core.ts).
142 |
143 | ### onCommitFiberRoot
144 |
145 | a utility function that wraps the `instrument` function and sets the `onCommitFiberRoot` hook.
146 |
147 | ```typescript
148 | import { onCommitFiberRoot } from 'bippy';
149 |
150 | onCommitFiberRoot((root) => {
151 | console.log('root ready to commit', root);
152 | });
153 | ```
154 |
155 | ### instrument
156 |
157 | > the underlying implementation for the `onCommitFiberRoot()` function. this is optional, unless you want to plug into more less common, advanced functionality.
158 |
159 | patches `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` with your handlers. must be imported before react, and must be initialized to properly run any other methods.
160 |
161 | > use with the `secure` function to prevent uncaught errors from crashing your app.
162 |
163 | ```typescript
164 | import { instrument, secure } from 'bippy'; // must be imported BEFORE react
165 | import * as React from 'react';
166 |
167 | instrument(
168 | secure({
169 | onCommitFiberRoot(rendererID, root) {
170 | console.log('root ready to commit', root);
171 | },
172 | onPostCommitFiberRoot(rendererID, root) {
173 | console.log('root with effects committed', root);
174 | },
175 | onCommitFiberUnmount(rendererID, fiber) {
176 | console.log('fiber unmounted', fiber);
177 | },
178 | })
179 | );
180 | ```
181 |
182 | ### getRDTHook
183 |
184 | returns the `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` object. great for advanced use cases, such as accessing or modifying the `renderers` property.
185 |
186 | ```typescript
187 | import { getRDTHook } from 'bippy';
188 |
189 | const hook = getRDTHook();
190 | console.log(hook);
191 | ```
192 |
193 | ### traverseRenderedFibers
194 |
195 | not every fiber in the fiber tree renders. `traverseRenderedFibers` allows you to traverse the fiber tree and determine which fibers have actually rendered.
196 |
197 | ```typescript
198 | import { instrument, secure, traverseRenderedFibers } from 'bippy'; // must be imported BEFORE react
199 | import * as React from 'react';
200 |
201 | instrument(
202 | secure({
203 | onCommitFiberRoot(rendererID, root) {
204 | traverseRenderedFibers(root, (fiber) => {
205 | console.log('fiber rendered', fiber);
206 | });
207 | },
208 | })
209 | );
210 | ```
211 |
212 | ### traverseFiber
213 |
214 | calls a callback on every fiber in the fiber tree.
215 |
216 | ```typescript
217 | import { instrument, secure, traverseFiber } from 'bippy'; // must be imported BEFORE react
218 | import * as React from 'react';
219 |
220 | instrument(
221 | secure({
222 | onCommitFiberRoot(rendererID, root) {
223 | traverseFiber(root.current, (fiber) => {
224 | console.log(fiber);
225 | });
226 | },
227 | })
228 | );
229 | ```
230 |
231 | ### traverseProps
232 |
233 | traverses the props of a fiber.
234 |
235 | ```typescript
236 | import { traverseProps } from 'bippy';
237 |
238 | // ...
239 |
240 | traverseProps(fiber, (propName, next, prev) => {
241 | console.log(propName, next, prev);
242 | });
243 | ```
244 |
245 | ### traverseState
246 |
247 | traverses the state (useState, useReducer, etc.) and effects that set state of a fiber.
248 |
249 | ```typescript
250 | import { traverseState } from 'bippy';
251 |
252 | // ...
253 |
254 | traverseState(fiber, (next, prev) => {
255 | console.log(next, prev);
256 | });
257 | ```
258 |
259 | ### traverseContexts
260 |
261 | traverses the contexts (useContext) of a fiber.
262 |
263 | ```typescript
264 | import { traverseContexts } from 'bippy';
265 |
266 | // ...
267 |
268 | traverseContexts(fiber, (next, prev) => {
269 | console.log(next, prev);
270 | });
271 | ```
272 |
273 | ### setFiberId / getFiberId
274 |
275 | set and get a persistent identity for a fiber. by default, fibers are anonymous and have no identity.
276 |
277 | ```typescript
278 | import { setFiberId, getFiberId } from 'bippy';
279 |
280 | // ...
281 |
282 | setFiberId(fiber);
283 | console.log('unique id for fiber:', getFiberId(fiber));
284 | ```
285 |
286 | ### isHostFiber
287 |
288 | returns `true` if the fiber is a host fiber (e.g., a DOM node in react-dom).
289 |
290 | ```typescript
291 | import { isHostFiber } from 'bippy';
292 |
293 | if (isHostFiber(fiber)) {
294 | console.log('fiber is a host fiber');
295 | }
296 | ```
297 |
298 | ### isCompositeFiber
299 |
300 | 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).
301 |
302 | ```typescript
303 | import { isCompositeFiber } from 'bippy';
304 |
305 | if (isCompositeFiber(fiber)) {
306 | console.log('fiber is a composite fiber');
307 | }
308 | ```
309 |
310 | ### getDisplayName
311 |
312 | returns the display name of the fiber's component, falling back to the component's function or class name if available.
313 |
314 | ```typescript
315 | import { getDisplayName } from 'bippy';
316 |
317 | console.log(getDisplayName(fiber));
318 | ```
319 |
320 | ### getType
321 |
322 | returns the underlying type (the component definition) for a given fiber. for example, this could be a function component or class component.
323 |
324 | ```jsx
325 | import { getType } from 'bippy';
326 | import { memo } from 'react';
327 |
328 | const RealComponent = () => {
329 | return
175 |
176 | bippy is a toolkit to{' '}
177 |
178 | hack into react internals
179 |
180 |
181 |
182 |
183 |
184 |
185 | by default, you cannot access react internals. bippy bypasses this
186 | by "pretending" to be react devtools, giving you access to the fiber
187 | tree and other internals.
188 |
189 |
190 |
191 |
192 |
193 |
194 | works outside of react – no react code modification needed
195 |
196 |
197 |
198 |
199 | utility functions that work across modern react (v17-19)
200 |
201 |
202 |
203 |
204 | no prior react source code knowledge required
205 |
206 |
207 |
208 |
209 |
210 |
211 | you can get started in {'<'}6 lines of code:
212 |
213 |
263 |
264 |
265 | ⚠️ warning:{' '}
266 |
267 |
268 | this project may break production apps and cause unexpected
269 | behavior
270 |
271 |
272 |
273 |
274 |
275 | this project uses react internals, which can change at any time.
276 | it is not recommended to depend on internals unless you really,{' '}
277 |
278 | really have to.
279 | {' '}
280 | by proceeding, you acknowledge the risk of breaking your own code
281 | or apps that use your code.
282 |
283 |