why-render-react 0.1.0 → 1.0.1

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.
Files changed (2) hide show
  1. package/README.md +189 -130
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,27 +1,18 @@
1
- # Why Render
1
+ # why-render-react
2
2
 
3
- React DevTools shows that something re-rendered. Why Render shows you why.
3
+ A tiny React hook that logs **why** a component re-rendered which props changed, what their previous and next values were, or whether nothing changed at all (meaning the re-render came from state, context, or a parent re-rendering).
4
4
 
5
- Add `useWhyRerender` to any component and get a console diff of exactly which props changed since the last render, so you can immediately tell whether a re-render was caused by a real prop change — or something else entirely.
5
+ It's built for the common, easy-to-miss case: a prop that *looks* the same but is actually a brand-new object, array, or function on every render.
6
6
 
7
- ## Table of Contents
7
+ ## Features
8
8
 
9
- - [Install](#install)
10
- - [Setup](#setup)
11
- - [Wiring up global setup](#wiring-up-global-setup)
12
- - [Usage](#usage)
13
- - [Options](#options)
14
- - [Examples](#examples)
15
- - [1. Basic component usage](#1-basic-component-usage)
16
- - [2. A component whose props change](#2-a-component-whose-props-change)
17
- - [3. A component that re-renders without its props changing](#3-a-component-that-re-renders-without-its-props-changing)
18
- - [4. Inspecting previous and next values with `verbose`](#4-inspecting-previous-and-next-values-with-verbose)
19
- - [5. Temporarily disabling tracking for one component](#5-temporarily-disabling-tracking-for-one-component)
20
- - [6. A realistic parent/child example](#6-a-realistic-parentchild-example)
21
- - [Expected Output](#expected-output)
22
- - [Troubleshooting](#troubleshooting)
9
+ - Zero-config logging of re-render causes, per component
10
+ - Shows exactly which props changed
11
+ - Optional `verbose` mode with previous/next values
12
+ - Per-component `isActive` toggle to silence noisy components without disabling everything
13
+ - One-line global `setup()` — works in dev, can be disabled in production
23
14
 
24
- ## Install
15
+ ## Installation
25
16
 
26
17
  ```bash
27
18
  npm install why-render-react
@@ -29,95 +20,78 @@ npm install why-render-react
29
20
 
30
21
  ## Setup
31
22
 
32
- Why Render is off by default. Call `setup()` once, before your app renders, to turn it on:
23
+ Call `setup()` once, before your app renders:
33
24
 
34
- ```ts
25
+ ```tsx
26
+ // index.tsx
35
27
  import { setup } from "why-render-react"
28
+ import { createRoot } from "react-dom/client"
29
+ import App from "./App"
36
30
 
37
31
  setup(true)
38
- ```
39
-
40
- `setup()` also accepts strings, so you can wire it to an environment variable:
41
-
42
- ```ts
43
- setup(process.env.USE_WHY_RENDER) // enabled only if the value is "true" (case-insensitive)
44
- ```
45
-
46
- Pass `false` (or leave it unset) to keep Why Render off:
47
32
 
48
- ```ts
49
- setup(false)
33
+ createRoot(document.getElementById("root")!).render(<App />)
50
34
  ```
51
35
 
52
- This is a one-time, app-wide switch call it once at your entry point, not inside individual components.
53
-
54
- > **Important:** if any component calls `useWhyRerender` before `setup()` has run, it will throw an error. Always call `setup()` first.
55
-
56
- ### Wiring up global setup
57
-
58
- `setup()` only needs to run once for your whole app — typically in your entry file, before rendering:
36
+ `setup()` takes a boolean (or anything that resolves to one, like an environment variable). A falsy value `setup(false)`, a missing env var, or a typo like `"tru"` — silently disables logging entirely.
59
37
 
60
38
  ```tsx
61
- // main.tsx
62
- import { setup } from "why-render-react"
63
- import { createRoot } from "react-dom/client"
64
- import App from "./App"
65
-
66
39
  setup(process.env.NODE_ENV !== "production")
67
-
68
- createRoot(document.getElementById("root")!).render(<App />)
69
40
  ```
70
41
 
71
- Every component that calls `useWhyRerender` anywhere in the tree will start (or stop) logging based on that single call. *(full file: [`examples/00-setup.tsx`](./examples/00-setup.tsx))*
72
-
73
- ## Usage
42
+ ## Basic Usage
74
43
 
75
- Add `useWhyRerender` inside any component you want to debug, passing it a name and that component's current props:
44
+ Call `useWhyRerender` inside any component you want to watch, passing a `name` and the `props` you want tracked:
76
45
 
77
46
  ```tsx
47
+ // CarCard.tsx
78
48
  import { useWhyRerender } from "why-render-react"
79
49
 
50
+ interface CarCardProps {
51
+ color: string
52
+ }
53
+
80
54
  function CarCard(props: CarCardProps) {
81
55
  useWhyRerender({ name: "CarCard", props })
82
-
83
56
  return <div>{props.color}</div>
84
57
  }
85
- ```
86
58
 
87
- It's a React hook, so it follows the same rules as other hooks — call it unconditionally, at the top level of your component. Every time `CarCard` renders, Why Render logs what changed (or didn't) since the last render.
88
-
89
- ## Options
90
-
91
- `useWhyRerender` takes a single options object:
92
-
93
- | Option | Type | Required | Default | What it does |
94
- | ------------ | ----------- | -------- | --------- | --------------------------------------------------------------------------------------------- |
95
- | `name` | `string` | Yes | — | The label shown for this component in the console output. |
96
- | `props` | `object` | Yes | — | The component's current props to track. |
97
- | `verbose` | `boolean` | No | `false` | Also logs each changed prop's previous and next value. |
98
- | `isActive` | `boolean` | No | `true` | Set to`false` to turn off logging for just this component, without affecting anything else. |
99
-
100
- > Note: the field is `isActive`. If your editor or docs show `isActivate` anywhere, that's an internal naming detail — the option you pass is `isActive`.
59
+ export default CarCard
60
+ ```
101
61
 
102
62
  ## Examples
103
63
 
104
- ### 1. Basic component usage
64
+ ### 1. First render
105
65
 
106
- *(full file: [`examples/01-basic-usage.tsx`](./examples/01-basic-usage.tsx))*
66
+ Nothing to compare yet, so it just logs the mount.
107
67
 
108
68
  ```tsx
69
+ import { useWhyRerender } from "why-render-react"
70
+
71
+ interface CarCardProps {
72
+ color: string
73
+ }
74
+
109
75
  function CarCard(props: CarCardProps) {
110
76
  useWhyRerender({ name: "CarCard", props })
111
-
112
77
  return <div>{props.color}</div>
113
78
  }
79
+
80
+ export default CarCard
114
81
  ```
115
82
 
116
- ### 2. A component whose props change
83
+ **Output:**
84
+
85
+ ```
86
+ [why-rerender] <CarCard> render #1 → initial mount
87
+ ```
117
88
 
118
- *(full file: [`examples/02-props-change.tsx`](./examples/02-props-change.tsx))*
89
+ ### 2. A prop actually changes
119
90
 
120
91
  ```tsx
92
+ import { useState } from "react"
93
+ import { useWhyRerender } from "why-render-react"
94
+
121
95
  function CarCard({ color }: { color: string }) {
122
96
  useWhyRerender({ name: "CarCard", props: { color } })
123
97
  return <div>{color}</div>
@@ -125,7 +99,6 @@ function CarCard({ color }: { color: string }) {
125
99
 
126
100
  function Garage() {
127
101
  const [color, setColor] = useState("blue")
128
-
129
102
  return (
130
103
  <>
131
104
  <button onClick={() => setColor("red")}>Repaint</button>
@@ -133,17 +106,25 @@ function Garage() {
133
106
  </>
134
107
  )
135
108
  }
109
+
110
+ export default Garage
136
111
  ```
137
112
 
138
- Clicking "Repaint" changes `color` and re-renders `CarCard`. The console shows `color` as the prop that changed confirming the re-render was caused by that prop.
113
+ **Output** (click "Repaint" — the line expands to show details):
139
114
 
140
- ### 3. A component that re-renders without its props changing
115
+ ```
116
+ ▶ [why-rerender] <CarCard> render #3 → 1 prop changed
117
+ CHANGED color
118
+ ```
141
119
 
142
- Components can re-render for reasons that have nothing to do with their own props — most commonly, because their parent re-rendered. This is exactly the case Why Render helps you catch:
120
+ ### 3. Re-render with no prop changes
143
121
 
144
- *(full file: [`examples/03-rerender-without-prop-change.tsx`](./examples/03-rerender-without-prop-change.tsx))*
122
+ The parent re-renders (e.g. from unrelated state), but the child's props never change.
145
123
 
146
124
  ```tsx
125
+ import { useState } from "react"
126
+ import { useWhyRerender } from "why-render-react"
127
+
147
128
  function CarCard({ color }: { color: string }) {
148
129
  useWhyRerender({ name: "CarCard", props: { color } })
149
130
  return <div>{color}</div>
@@ -151,72 +132,133 @@ function CarCard({ color }: { color: string }) {
151
132
 
152
133
  function Garage() {
153
134
  const [, forceTick] = useState(0)
154
-
155
135
  return (
156
136
  <>
157
- {/* Re-renders Garage (and CarCard) every second, but color never changes */}
137
+ {/* Re-renders Garage (and CarCard) on every click, but color never changes */}
158
138
  <button onClick={() => forceTick((t) => t + 1)}>Refresh</button>
159
139
  <CarCard color="blue" />
160
140
  </>
161
141
  )
162
142
  }
143
+
144
+ export default Garage
163
145
  ```
164
146
 
165
- Clicking "Refresh" re-renders `Garage`, which re-renders `CarCard` even though `color` is still `"blue"`. Why Render logs that no props changed on that render — telling you to look at the parent, state, or context instead of the props.
147
+ **Output:**
166
148
 
167
- ### 4. Inspecting previous and next values with `verbose`
149
+ ```
150
+ [why-rerender] <CarCard> render #4 → no prop changes — check state, context, or parent re-render
151
+ ```
168
152
 
169
- *(full file: [`examples/04-verbose.tsx`](./examples/04-verbose.tsx))*
153
+ ### 4. Verbose mode
154
+
155
+ Set `verbose: true` to see the previous and next values alongside each changed prop.
170
156
 
171
157
  ```tsx
158
+ import { useState } from "react"
159
+ import { useWhyRerender } from "why-render-react"
160
+
172
161
  function CarCard({ color }: { color: string }) {
173
162
  useWhyRerender({ name: "CarCard", props: { color }, verbose: true })
174
163
  return <div>{color}</div>
175
164
  }
165
+
166
+ function Garage() {
167
+ const [color, setColor] = useState("blue")
168
+ return (
169
+ <>
170
+ <button onClick={() => setColor("red")}>Repaint</button>
171
+ <CarCard color={color} />
172
+ </>
173
+ )
174
+ }
175
+
176
+ export default Garage
176
177
  ```
177
178
 
178
- With `verbose: true`, each changed prop's previous and next value is included in the output, so you don't have to guess what it changed *from* and *to*.
179
+ **Output:**
179
180
 
180
- ### 5. Temporarily disabling tracking for one component
181
+ ```
182
+ ▶ [why-rerender] <CarCard> render #3 → 1 prop changed
183
+ CHANGED color { prev: "blue", next: "red" }
184
+ ```
185
+
186
+ ### 5. Silencing one noisy component
181
187
 
182
- *(full file: [`examples/05-disable-instance.tsx`](./examples/05-disable-instance.tsx))*
188
+ If a single component logs too often to debug around, set `isActive: false` on that one instead of disabling `setup()` for the whole app.
183
189
 
184
190
  ```tsx
191
+ import { useWhyRerender } from "why-render-react"
192
+
193
+ interface NoisyWidgetProps {
194
+ cursorX: number
195
+ cursorY: number
196
+ }
197
+
185
198
  function NoisyWidget(props: NoisyWidgetProps) {
186
199
  useWhyRerender({ name: "NoisyWidget", props, isActive: false })
187
- return (/* ... */)
200
+ return (
201
+ <div>
202
+ {props.cursorX},{props.cursorY}
203
+ </div>
204
+ )
188
205
  }
189
- ```
190
206
 
191
- Use this when one component re-renders constantly (e.g. on every keystroke) and its logging is drowning out everything else. Setting `isActive: false` only silences `NoisyWidget` — `setup()` stays enabled, and every other component's `useWhyRerender` call keeps logging normally:
192
-
193
- ```tsx
194
- function Dashboard(props: DashboardProps) {
195
- useWhyRerender({ name: "Dashboard", props }) // still logs
207
+ function Sidebar(props: { title: string }) {
208
+ useWhyRerender({ name: "Sidebar", props })
209
+ return <aside>{props.title}</aside>
210
+ }
196
211
 
212
+ function Dashboard(props: { title: string }) {
213
+ useWhyRerender({ name: "Dashboard", props })
197
214
  return (
198
215
  <>
199
- <Sidebar {...sidebarProps} /> {/* still logs */}
200
- <NoisyWidget {...widgetProps} /> {/* silenced */}
216
+ <Sidebar title={props.title} /> {/* still logs */}
217
+ <NoisyWidget cursorX={0} cursorY={0} /> {/* silenced */}
201
218
  </>
202
219
  )
203
220
  }
221
+
222
+ export default Dashboard
204
223
  ```
205
224
 
206
- ### 6. A realistic parent/child example
225
+ ### 6. The hidden cause: a new function every render
207
226
 
208
- *(full file: [`examples/06-parent-child.tsx`](./examples/06-parent-child.tsx))*
227
+ This is the case `why-render-react` is best at catching. `CarList` passes a brand-new `onSelect` arrow function to every `CarCard` on every render. Even though `id` and `color` might not have changed, `onSelect` is a *different value* in JavaScript each time — so Why Render correctly flags it as changed.
209
228
 
210
229
  ```tsx
211
- function CarCard({ id, color, onSelect }: CarCardProps) {
212
- useWhyRerender({ name: "CarCard", props: { id, color, onSelect }, verbose: true })
230
+ import { useState } from "react"
231
+ import { useWhyRerender } from "why-render-react"
232
+
233
+ interface Car {
234
+ id: number
235
+ color: string
236
+ }
213
237
 
238
+ interface CarCardProps {
239
+ id: number
240
+ color: string
241
+ onSelect: (id: number) => void
242
+ }
243
+
244
+ function CarCard({ id, color, onSelect }: CarCardProps) {
245
+ useWhyRerender({
246
+ name: "CarCard",
247
+ props: { id, color, onSelect },
248
+ verbose: true,
249
+ })
214
250
  return <div onClick={() => onSelect(id)}>{color}</div>
215
251
  }
216
252
 
217
253
  function CarList({ cars }: { cars: Car[] }) {
218
254
  const [selectedId, setSelectedId] = useState<number | null>(null)
219
255
 
256
+ const setId = (id: number) => {
257
+ if (selectedId !== id) {
258
+ setSelectedId(id)
259
+ }
260
+ }
261
+
220
262
  return (
221
263
  <>
222
264
  {cars.map((car) => (
@@ -224,67 +266,84 @@ function CarList({ cars }: { cars: Car[] }) {
224
266
  key={car.id}
225
267
  id={car.id}
226
268
  color={car.color}
227
- // A new function is created on every CarList render, so this prop
228
- // "changes" on every render even though the click behavior is the same.
229
- onSelect={(id) => setSelectedId(id)}
269
+ // New function every render -> onSelect always shows as "changed"
270
+ onSelect={(id) => setId(id)}
230
271
  />
231
272
  ))}
232
273
  </>
233
274
  )
234
275
  }
235
- ```
236
276
 
237
- Every time `CarList` re-renders (e.g. when `selectedId` changes), each `CarCard` gets a brand-new `onSelect` function. Why Render flags `onSelect` as changed on every render — a common, easy-to-miss cause of unnecessary re-renders that's hard to spot just by eyeballing the code.
238
-
239
- ## Expected Output
277
+ export default CarList
278
+ ```
240
279
 
241
- **First render — nothing to compare yet:**
280
+ **Output:**
242
281
 
243
282
  ```
244
- [why-rerender] <CarCard> render #1initial mount
283
+ [why-rerender] <CarCard> render #21 prop changed
284
+ CHANGED onSelect { prev: "ƒ", next: "ƒ" }
245
285
  ```
246
286
 
247
- **A later render where a prop actually changed** (this line expands to show details):
287
+ **The fix** memoize the handler with `useCallback` so the same function reference is reused across renders:
248
288
 
249
- ```
250
- [why-rerender] <CarCard> render #3 1 prop changed
251
- CHANGED color
252
- ```
289
+ ```tsx
290
+ import { useState, useCallback } from "react"
291
+ import { useWhyRerender } from "why-render-react"
253
292
 
254
- With `verbose: true`, the same line also includes the previous and next values:
293
+ function CarList({ cars }: { cars: Car[] }) {
294
+ const [selectedId, setSelectedId] = useState<number | null>(null)
255
295
 
256
- ```
257
- [why-rerender] <CarCard> render #3 1 prop changed
258
- CHANGED color { prev: "blue", next: "red" }
296
+ const setId = useCallback((id: number) => {
297
+ setSelectedId((current) => (current !== id ? id : current))
298
+ }, [])
299
+
300
+ return (
301
+ <>
302
+ {cars.map((car) => (
303
+ <CarCard key={car.id} id={car.id} color={car.color} onSelect={setId} />
304
+ ))}
305
+ </>
306
+ )
307
+ }
259
308
  ```
260
309
 
261
- **A render where no props changed** (see [Example 3](#3-a-component-that-re-renders-without-its-props-changing)):
310
+ ## API Reference
262
311
 
263
- ```
264
- [why-rerender] <CarCard> render #4 → no prop changes — check state, context, or parent re-render
265
- ```
312
+ ### `setup(enabled: boolean)`
266
313
 
267
- ## Troubleshooting
314
+ Enables or disables all Why Render logging globally. Must be called once, before your app renders.
315
+
316
+ ### `useWhyRerender(options)`
268
317
 
269
- **Error: "WhyRenderConfig has not been configured"** `setup()` hasn't run yet. Call it once, before your app renders (see [Setup](#setup)).
318
+ | Option | Type | Required | Default | Description |
319
+ |-----------|-----------------------|----------|---------|-------------------------------------------------------------------------------|
320
+ | `name` | `string` | Yes | — | Label shown in the console log for this component. |
321
+ | `props` | `object` | Yes | — | The props (or any values) to track for changes. |
322
+ | `isActive`| `boolean` | No | `true` | Set to `false` to silence logging for just this component. |
323
+ | `verbose` | `boolean` | No | `false` | Includes previous/next values for each changed prop in the log. |
270
324
 
271
- **Nothing is logging, and no error was thrown** — check:
325
+ ## Troubleshooting
272
326
 
273
- 1. `setup()` was actually called with a value that resolves to enabled — `setup(false)`, a missing environment variable, or a typo like `"tru"` will silently disable it.
274
- 2. That component's call doesn't have `isActive: false` set (see [Example 5](#5-temporarily-disabling-tracking-for-one-component)).
327
+ **Error: `"WhyRenderConfig has not been configured"`**
328
+ `setup()` hasn't run yet. Call it once, before your app renders (see [Setup](#setup)).
275
329
 
276
- **One component is too noisy to debug around** — set `isActive: false` on that component instead of disabling `setup()` for the whole app.
330
+ **Nothing is logging, and no error was thrown**
331
+ Check the following:
332
+ - `setup()` was actually called with a value that resolves to `true` — `setup(false)`, a missing environment variable, or a typo like `"tru"` will silently disable it.
333
+ - That component's call doesn't have `isActive: false` set (see [Example 5](#5-silencing-one-noisy-component)).
334
+ - If one component is too noisy to debug around, set `isActive: false` on that component instead of disabling `setup()` for the whole app.
277
335
 
278
- **A prop keeps showing as "changed" even though it looks the same** — this usually means a new object, array, or function is being created for that prop on every render (see [Example 6](#6-a-realistic-parentchild-example)). Since these are different values in JavaScript even when their contents look identical, Why Render correctly reports them as changed.
336
+ **A prop keeps showing as "changed" even though it looks the same**
337
+ This usually means a new object, array, or function is being created for that prop on every render (see [Example 6](#6-the-hidden-cause-a-new-function-every-render)). Since these are different values in JavaScript even when their contents look identical, Why Render correctly reports them as changed.
279
338
 
280
- # License
339
+ ## License
281
340
 
282
- ### MIT License
341
+ MIT License
283
342
 
284
- Copyright (c) why-render contributors
343
+ Copyright (c) why-render-react contributors
285
344
 
286
345
  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:
287
346
 
288
347
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
289
348
 
290
- 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.
349
+ 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "why-render-react",
3
- "version": "0.1.0",
3
+ "version": "1.0.1",
4
4
  "description": "A React hook that helps you understand why a component re-rendered.",
5
5
  "types": "module",
6
6
  "main": "dist/index.js",