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 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
- a hacky way to get fibers from react. used internally for [`react-scan`](https://github.com/aidenybai/react-scan).
8
+ [![size](https://img.shields.io/bundlephobia/minzip/bippy?label=gzip&style=flat&colorA=000000&colorB=000000)](https://bundlephobia.com/package/bippy)
9
+ [![version](https://img.shields.io/npm/v/bippy?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/bippy)
10
+ [![downloads](https://img.shields.io/npm/dt/bippy.svg?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/bippy)
4
11
 
5
- bippy works by monkey-patching `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` with [custom handlers](https://github.com/facebook/react/blob/6a4b46cd70d2672bc4be59dcb5b8dede22ed0cef/packages/react-refresh/src/ReactFreshRuntime.js#L427). this gives us access to react internals without needing to use react devtools.
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
- > [!WARNING]
8
- > this project uses react internals, which can change at any time. **this is not recommended for usage and may break production apps** - unless you acknowledge this risk and know exactly you're doing.
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
- ## tutorial: create a mini react-scan
23
+ ## how it works
11
24
 
12
- [`react-scan`](https://github.com/aidenybai/react-scan) is a tool that highlights renders in your react app. under the hood, it uses bippy to detect rendered fibers.
25
+ bippy allows you to **access** and **use** fibers from outside of react.
13
26
 
14
- fibers are how "work" is represented in react. each fiber either represents a composite (function/class component) or a host (dom element). [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/).
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 simplified version of a fiber looks roughly like this:
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 everythign and ready to apply changes to the host tree (e.g. DOM mutations)
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
- fiber: Record<string, unknown>,
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
- finally, re-run the dev server:
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
- instrument({
114
- onCommitFiberRoot(rendererID, root) {
115
- visit(rendererID, root);
116
- },
117
- });
94
+ // called when a specific fiber unmounts
95
+ onCommitFiberUnmount: (rendererID: RendererID, Fiber: Fiber) => void;
96
+ }
118
97
  ```
119
98
 
120
- ### 3. determine DOM nodes to highlight
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
- next, we need to identify which DOM nodes we are going to highlight. we can do this by checking if the fiber is a host fiber, or if it's not, find the nearest host fiber.
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
- ```jsx
125
- import {
126
- instrument,
127
- isHostFiber,
128
- getNearestHostFiber,
129
- createFiberVisitor,
130
- } from 'bippy'; // must be imported BEFORE react
106
+ ## examples
131
107
 
132
- // rest of your code ...
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
- const highlightFiber = (fiber) => {
135
- if (!(fiber instanceof HTMLElement)) return;
110
+ ### a mini react-scan
136
111
 
137
- console.log('highlight dom node', fiber.stateNode);
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
- ### 4. highlight DOM nodes
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
- if (isHostFiber(fiber)) {
194
- highlightFiber(fiber);
195
- } else {
196
- // can be a component
197
- const hostFiber = getNearestHostFiber(fiber);
198
- highlightFiber(hostFiber);
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
- instrument({
204
- onCommitFiberRoot(rendererID, root) {
205
- visit(rendererID, root);
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
- ### 5. profit
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
- try a completed version [here](https://bippy.million.dev)
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
- you can learn more about bippy by [reading the source code](https://github.com/aidenybai/bippy/blob/main/src/index.ts).
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
- looking for a more robust version of our mini react-scan? try out [react-scan](https://github.com/aidenybai/react-scan).
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.