bippy 0.0.24 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +200 -129
- package/dist/index.cjs +205 -139
- package/dist/index.d.cts +154 -72
- package/dist/index.d.ts +154 -72
- package/dist/index.global.js +2 -2
- package/dist/index.js +199 -108
- package/package.json +83 -80
package/README.md
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
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
|
+
|
|
1
6
|
# <img src="https://github.com/aidenybai/bippy/blob/main/.github/assets/bippy.png?raw=true" width="60" align="center" /> bippy
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
[](https://bundlephobia.com/package/bippy)
|
|
9
|
+
[](https://npmjs.com/package/bippy)
|
|
10
|
+
[](https://npmjs.com/package/bippy)
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
a hacky way to get fibers from react. <small>used internally by [`react-scan`](https://github.com/aidenybai/react-scan)</small>
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
|
|
14
|
+
bippy _attempts\*_ to solve two problems:
|
|
15
|
+
|
|
16
|
+
1. it's not possible to write instrumentation for React without the end user changing code
|
|
17
|
+
2. doing anything useful with fibers requires you to know react source code very well
|
|
18
|
+
|
|
19
|
+
bippy allows you to access fiber information from outside of react and provides friendly low-level utils for interacting with fibers.
|
|
20
|
+
|
|
21
|
+
<sub><sup>\*disclaimer: "attempt" used loosely, i highly recommend not relying on this in production</sub></sup>
|
|
9
22
|
|
|
10
|
-
##
|
|
23
|
+
## how it works
|
|
11
24
|
|
|
12
|
-
|
|
25
|
+
bippy allows you to **access** and **use** fibers from outside of react.
|
|
13
26
|
|
|
14
|
-
|
|
27
|
+
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).
|
|
15
28
|
|
|
16
|
-
a
|
|
29
|
+
> 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/).
|
|
30
|
+
|
|
31
|
+
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:
|
|
17
32
|
|
|
18
33
|
```typescript
|
|
19
34
|
interface Fiber {
|
|
@@ -23,6 +38,9 @@ interface Fiber {
|
|
|
23
38
|
child: Fiber | null;
|
|
24
39
|
sibling: Fiber | null;
|
|
25
40
|
|
|
41
|
+
// stateNode is the host fiber (e.g. DOM element)
|
|
42
|
+
stateNode: Node | null;
|
|
43
|
+
|
|
26
44
|
// parent fiber
|
|
27
45
|
return: Fiber | null;
|
|
28
46
|
|
|
@@ -34,12 +52,26 @@ interface Fiber {
|
|
|
34
52
|
|
|
35
53
|
// contexts (useContext)
|
|
36
54
|
dependencies: Dependencies | null;
|
|
55
|
+
|
|
56
|
+
// effects (useEffect, useLayoutEffect, etc.)
|
|
57
|
+
updateQueue: any;
|
|
37
58
|
}
|
|
38
59
|
```
|
|
39
60
|
|
|
61
|
+
here, the `child`, `sibling`, and `return` properties are pointers to other fibers in the tree.
|
|
62
|
+
|
|
63
|
+
additionally, `memoizedProps`, `memoizedState`, and `dependencies` are the fiber's props, state, and contexts.
|
|
64
|
+
|
|
65
|
+
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:
|
|
66
|
+
|
|
67
|
+
- `createFiberVisitor` to detect renders and `traverseFiber` to traverse the overall fiber tree
|
|
68
|
+
- _(instead of `child`, `sibling`, and `return` pointers)_
|
|
69
|
+
- `traverseProps`, `traverseState`, and `traverseContexts` to traverse the fiber's props, state, and contexts
|
|
70
|
+
- _(instead of `memoizedProps`, `memoizedState`, and `dependencies`)_
|
|
71
|
+
|
|
40
72
|
however, fibers aren't directly accessible by the user. so, we have to hack our way around to accessing it.
|
|
41
73
|
|
|
42
|
-
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 is intended for react devtools, but we can use it to our advantage.
|
|
74
|
+
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.
|
|
43
75
|
|
|
44
76
|
here's what it roughly looks like:
|
|
45
77
|
|
|
@@ -48,119 +80,38 @@ interface __REACT_DEVTOOLS_GLOBAL_HOOK__ {
|
|
|
48
80
|
// list of renderers (react-dom, react-native, etc.)
|
|
49
81
|
renderers: Map<RendererID, ReactRenderer>;
|
|
50
82
|
|
|
51
|
-
// called when react has rendered
|
|
83
|
+
// called when react has rendered everything for an update and is ready to
|
|
84
|
+
// apply changes to the host tree (e.g. DOM mutations)
|
|
52
85
|
onCommitFiberRoot: (
|
|
53
86
|
rendererID: RendererID,
|
|
54
|
-
|
|
55
|
-
commitPriority?: number
|
|
56
|
-
didError?: boolean,
|
|
87
|
+
root: FiberRoot,
|
|
88
|
+
commitPriority?: number
|
|
57
89
|
) => void;
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
we can use bippy's utils and the `onCommitFiberRoot` handler to detect renders!
|
|
62
|
-
|
|
63
|
-
### 0. setup
|
|
64
|
-
|
|
65
|
-
first, [create a new react project via stackblitz](https://stackblitz.com/fork/github/vitejs/vite/tree/main/packages/create-vite/template-react?file=src/App.jsx&terminal=dev)
|
|
66
|
-
|
|
67
|
-
then, install bippy:
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
npm install bippy
|
|
71
|
-
```
|
|
72
90
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
npm run dev
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### 1. use `onCommitFiberRoot` to get fibers
|
|
80
|
-
|
|
81
|
-
let's use `instrument` to stub the `__REACT_DEVTOOLS_GLOBAL_HOOK__` object, and setup a custom handler for `onCommitFiberRoot`.
|
|
82
|
-
|
|
83
|
-
```jsx
|
|
84
|
-
import { instrument } from 'bippy'; // must be imported BEFORE react
|
|
85
|
-
|
|
86
|
-
// rest of your code ...
|
|
87
|
-
|
|
88
|
-
instrument({
|
|
89
|
-
onCommitFiberRoot(rendererID, root) {
|
|
90
|
-
const fiberRoot = root.current;
|
|
91
|
-
console.log('fiberRoot', fiberRoot);
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
running this should log `fiberRoot` to the console. i recommend you playing with this code to get a feel for how fibers work.
|
|
97
|
-
|
|
98
|
-
### 2. create a fiber visitor
|
|
99
|
-
|
|
100
|
-
now, let's create a fiber visitor with `createFiberVisitor` to "visit" fibers that render. not every fiber actually renders, so we need to filter for the ones that do.
|
|
101
|
-
|
|
102
|
-
```jsx
|
|
103
|
-
import { instrument, createFiberVisitor } from 'bippy'; // must be imported BEFORE react
|
|
104
|
-
|
|
105
|
-
// rest of your code ...
|
|
106
|
-
|
|
107
|
-
const visit = createFiberVisitor({
|
|
108
|
-
onRender(fiber) {
|
|
109
|
-
console.log('fiber render', fiber);
|
|
110
|
-
},
|
|
111
|
-
});
|
|
91
|
+
// called when effects run
|
|
92
|
+
onPostCommitFiberRoot: (rendererID: RendererID, root: FiberRoot) => void;
|
|
112
93
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
},
|
|
117
|
-
});
|
|
94
|
+
// called when a specific fiber unmounts
|
|
95
|
+
onCommitFiberUnmount: (rendererID: RendererID, Fiber: Fiber) => void;
|
|
96
|
+
}
|
|
118
97
|
```
|
|
119
98
|
|
|
120
|
-
|
|
99
|
+
bippy works by monkey-patching `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` with our own custom handlers. bippy simplifies this by providing utility functions like:
|
|
121
100
|
|
|
122
|
-
|
|
101
|
+
- `instrument` to safely patch `window.__REACT_DEVTOOLS_GLOBAL_HOOK__`
|
|
102
|
+
- _(instead of directly mutating `onCommitFiberRoot`, ...)_
|
|
103
|
+
- `secure` to wrap your handlers in a try/catch and determine if handlers are safe to run
|
|
104
|
+
- _(instead of rawdogging `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` handlers, which may crash your app)_
|
|
123
105
|
|
|
124
|
-
|
|
125
|
-
import {
|
|
126
|
-
instrument,
|
|
127
|
-
isHostFiber,
|
|
128
|
-
getNearestHostFiber,
|
|
129
|
-
createFiberVisitor,
|
|
130
|
-
} from 'bippy'; // must be imported BEFORE react
|
|
106
|
+
## examples
|
|
131
107
|
|
|
132
|
-
|
|
108
|
+
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:
|
|
133
109
|
|
|
134
|
-
|
|
135
|
-
if (!(fiber instanceof HTMLElement)) return;
|
|
110
|
+
### a mini react-scan
|
|
136
111
|
|
|
137
|
-
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const visit = createFiberVisitor({
|
|
141
|
-
onRender(fiber) {
|
|
142
|
-
if (isHostFiber(fiber)) {
|
|
143
|
-
highlightFiber(fiber);
|
|
144
|
-
} else {
|
|
145
|
-
// can be a component
|
|
146
|
-
const hostFiber = getNearestHostFiber(fiber);
|
|
147
|
-
highlightFiber(hostFiber);
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
instrument({
|
|
153
|
-
onCommitFiberRoot(rendererID, root) {
|
|
154
|
-
visit(rendererID, root);
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
```
|
|
112
|
+
here's a mini toy version of [`react-scan`](https://github.com/aidenybai/react-scan) that highlights renders in your app.
|
|
158
113
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
now, let's implement the `highlightFiber` function to highlight the DOM node. the simplest way is to just overlay a div (with a red border) on top of the DOM node.
|
|
162
|
-
|
|
163
|
-
```jsx
|
|
114
|
+
```javascript
|
|
164
115
|
import {
|
|
165
116
|
instrument,
|
|
166
117
|
isHostFiber,
|
|
@@ -168,11 +119,9 @@ import {
|
|
|
168
119
|
createFiberVisitor,
|
|
169
120
|
} from 'bippy'; // must be imported BEFORE react
|
|
170
121
|
|
|
171
|
-
// rest of your code ...
|
|
172
|
-
|
|
173
122
|
const highlightFiber = (fiber) => {
|
|
174
123
|
if (!(fiber.stateNode instanceof HTMLElement)) return;
|
|
175
|
-
|
|
124
|
+
// fiber.stateNode is a DOM element
|
|
176
125
|
const rect = fiber.stateNode.getBoundingClientRect();
|
|
177
126
|
const highlight = document.createElement('div');
|
|
178
127
|
highlight.style.border = '1px solid red';
|
|
@@ -188,33 +137,155 @@ const highlightFiber = (fiber) => {
|
|
|
188
137
|
}, 100);
|
|
189
138
|
};
|
|
190
139
|
|
|
140
|
+
/**
|
|
141
|
+
* `createFiberVisitor` traverses the fiber tree and determines which
|
|
142
|
+
* fibers have actually rendered.
|
|
143
|
+
*
|
|
144
|
+
* A fiber tree contains many fibers that may have not rendered. this
|
|
145
|
+
* can be because it bailed out (e.g. `useMemo`) or because it wasn't
|
|
146
|
+
* actually rendered (if <Child> re-rendered, then <Parent> didn't
|
|
147
|
+
* actually render, but exists in the fiber tree).
|
|
148
|
+
*/
|
|
191
149
|
const visit = createFiberVisitor({
|
|
192
150
|
onRender(fiber) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
151
|
+
/**
|
|
152
|
+
* `getNearestHostFiber` is a utility function that finds the
|
|
153
|
+
* nearest host fiber to a given fiber.
|
|
154
|
+
*
|
|
155
|
+
* a host fiber for `react-dom` is a fiber that has a DOM element
|
|
156
|
+
* as its `stateNode`.
|
|
157
|
+
*/
|
|
158
|
+
const hostFiber = getNearestHostFiber(fiber);
|
|
159
|
+
highlightFiber(hostFiber);
|
|
200
160
|
},
|
|
201
161
|
});
|
|
202
162
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
163
|
+
/**
|
|
164
|
+
* `instrument` is a function that installs the React DevTools global
|
|
165
|
+
* hook and allows you to set up custom handlers for React fiber events.
|
|
166
|
+
*/
|
|
167
|
+
instrument(
|
|
168
|
+
/**
|
|
169
|
+
* `secure` is a function that wraps your handlers in a try/catch
|
|
170
|
+
* and prevents it from crashing the app. it also prevents it from
|
|
171
|
+
* running on unsupported React versions and during production.
|
|
172
|
+
*
|
|
173
|
+
* this is not required but highly recommended to provide "safeguards"
|
|
174
|
+
* in case something breaks.
|
|
175
|
+
*/
|
|
176
|
+
secure({
|
|
177
|
+
/**
|
|
178
|
+
* `onCommitFiberRoot` is a handler that is called when React is
|
|
179
|
+
* ready to commit a fiber root. this means that React is has
|
|
180
|
+
* rendered your entire app and is ready to apply changes to
|
|
181
|
+
* the host tree (e.g. via DOM mutations).
|
|
182
|
+
*/
|
|
183
|
+
onCommitFiberRoot(rendererID, root) {
|
|
184
|
+
visit(rendererID, root);
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
);
|
|
208
188
|
```
|
|
209
189
|
|
|
210
|
-
###
|
|
190
|
+
### a mini why-did-you-render
|
|
191
|
+
|
|
192
|
+
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.
|
|
211
193
|
|
|
212
|
-
|
|
194
|
+
```typescript
|
|
195
|
+
import {
|
|
196
|
+
instrument,
|
|
197
|
+
isHostFiber,
|
|
198
|
+
createFiberVisitor,
|
|
199
|
+
isCompositeFiber,
|
|
200
|
+
getDisplayName,
|
|
201
|
+
traverseProps,
|
|
202
|
+
traverseContexts,
|
|
203
|
+
traverseState,
|
|
204
|
+
} from 'bippy'; // must be imported BEFORE react
|
|
213
205
|
|
|
214
|
-
|
|
206
|
+
const visit = createFiberVisitor({
|
|
207
|
+
onRender(fiber) {
|
|
208
|
+
/**
|
|
209
|
+
* `isCompositeFiber` is a utility function that checks if a fiber is a composite fiber.
|
|
210
|
+
* a composite fiber is a fiber that represents a function or class component.
|
|
211
|
+
*/
|
|
212
|
+
if (!isCompositeFiber(fiber)) return;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* `getDisplayName` is a utility function that gets the display name of a fiber.
|
|
216
|
+
*/
|
|
217
|
+
const displayName = getDisplayName(fiber);
|
|
218
|
+
if (!displayName) return;
|
|
219
|
+
|
|
220
|
+
const changes = [];
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* `traverseProps` is a utility function that traverses the props of a fiber.
|
|
224
|
+
*/
|
|
225
|
+
traverseProps(fiber, (propName, next, prev) => {
|
|
226
|
+
if (next !== prev) {
|
|
227
|
+
changes.push({
|
|
228
|
+
name: `prop ${propName}`,
|
|
229
|
+
prev,
|
|
230
|
+
next,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
let contextId = 0;
|
|
236
|
+
/**
|
|
237
|
+
* `traverseContexts` is a utility function that traverses the contexts of a fiber.
|
|
238
|
+
* Contexts don't have a "name" like props, so we use an id to identify them.
|
|
239
|
+
*/
|
|
240
|
+
traverseContexts(fiber, (next, prev) => {
|
|
241
|
+
if (next !== prev) {
|
|
242
|
+
changes.push({
|
|
243
|
+
name: `context ${contextId}`,
|
|
244
|
+
prev,
|
|
245
|
+
next,
|
|
246
|
+
contextId,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
contextId++;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
let stateId = 0;
|
|
253
|
+
/**
|
|
254
|
+
* `traverseState` is a utility function that traverses the state of a fiber.
|
|
255
|
+
*
|
|
256
|
+
* State don't have a "name" like props, so we use an id to identify them.
|
|
257
|
+
*/
|
|
258
|
+
traverseState(fiber, (value, prevValue) => {
|
|
259
|
+
if (next !== prev) {
|
|
260
|
+
changes.push({
|
|
261
|
+
name: `state ${stateId}`,
|
|
262
|
+
prev,
|
|
263
|
+
next,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
stateId++;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
console.group(
|
|
270
|
+
`%c${displayName}`,
|
|
271
|
+
'background: hsla(0,0%,70%,.3); border-radius:3px; padding: 0 2px;'
|
|
272
|
+
);
|
|
273
|
+
for (const { name, prev, next } of changes) {
|
|
274
|
+
console.log(`${name}:`, prev, '!==', next);
|
|
275
|
+
}
|
|
276
|
+
console.groupEnd();
|
|
277
|
+
},
|
|
278
|
+
});
|
|
215
279
|
|
|
216
|
-
|
|
280
|
+
instrument(
|
|
281
|
+
secure({
|
|
282
|
+
onCommitFiberRoot(rendererID, root) {
|
|
283
|
+
visit(rendererID, root);
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
```
|
|
217
288
|
|
|
218
289
|
## misc
|
|
219
290
|
|
|
220
|
-
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
|
|
291
|
+
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.
|