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.
- package/README.md +189 -130
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# why-render-react
|
|
2
2
|
|
|
3
|
-
React
|
|
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
|
-
|
|
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
|
-
##
|
|
7
|
+
## Features
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
23
|
+
Call `setup()` once, before your app renders:
|
|
33
24
|
|
|
34
|
-
```
|
|
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
|
-
|
|
49
|
-
setup(false)
|
|
33
|
+
createRoot(document.getElementById("root")!).render(<App />)
|
|
50
34
|
```
|
|
51
35
|
|
|
52
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
## Usage
|
|
42
|
+
## Basic Usage
|
|
74
43
|
|
|
75
|
-
|
|
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
|
-
|
|
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.
|
|
64
|
+
### 1. First render
|
|
105
65
|
|
|
106
|
-
|
|
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
|
-
|
|
83
|
+
**Output:**
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
[why-rerender] <CarCard> render #1 → initial mount
|
|
87
|
+
```
|
|
117
88
|
|
|
118
|
-
|
|
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
|
-
|
|
113
|
+
**Output** (click "Repaint" — the line expands to show details):
|
|
139
114
|
|
|
140
|
-
|
|
115
|
+
```
|
|
116
|
+
▶ [why-rerender] <CarCard> render #3 → 1 prop changed
|
|
117
|
+
CHANGED color
|
|
118
|
+
```
|
|
141
119
|
|
|
142
|
-
|
|
120
|
+
### 3. Re-render with no prop changes
|
|
143
121
|
|
|
144
|
-
|
|
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
|
|
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
|
-
|
|
147
|
+
**Output:**
|
|
166
148
|
|
|
167
|
-
|
|
149
|
+
```
|
|
150
|
+
[why-rerender] <CarCard> render #4 → no prop changes — check state, context, or parent re-render
|
|
151
|
+
```
|
|
168
152
|
|
|
169
|
-
|
|
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
|
-
|
|
179
|
+
**Output:**
|
|
179
180
|
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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 {
|
|
200
|
-
<NoisyWidget {
|
|
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.
|
|
225
|
+
### 6. The hidden cause: a new function every render
|
|
207
226
|
|
|
208
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
//
|
|
228
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
## Expected Output
|
|
277
|
+
export default CarList
|
|
278
|
+
```
|
|
240
279
|
|
|
241
|
-
**
|
|
280
|
+
**Output:**
|
|
242
281
|
|
|
243
282
|
```
|
|
244
|
-
[why-rerender] <CarCard> render #
|
|
283
|
+
▶ [why-rerender] <CarCard> render #2 → 1 prop changed
|
|
284
|
+
CHANGED onSelect { prev: "ƒ", next: "ƒ" }
|
|
245
285
|
```
|
|
246
286
|
|
|
247
|
-
**
|
|
287
|
+
**The fix** — memoize the handler with `useCallback` so the same function reference is reused across renders:
|
|
248
288
|
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
```
|
|
289
|
+
```tsx
|
|
290
|
+
import { useState, useCallback } from "react"
|
|
291
|
+
import { useWhyRerender } from "why-render-react"
|
|
253
292
|
|
|
254
|
-
|
|
293
|
+
function CarList({ cars }: { cars: Car[] }) {
|
|
294
|
+
const [selectedId, setSelectedId] = useState<number | null>(null)
|
|
255
295
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
+
Enables or disables all Why Render logging globally. Must be called once, before your app renders.
|
|
315
|
+
|
|
316
|
+
### `useWhyRerender(options)`
|
|
268
317
|
|
|
269
|
-
|
|
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
|
-
|
|
325
|
+
## Troubleshooting
|
|
272
326
|
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
**
|
|
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**
|
|
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
|
-
|
|
339
|
+
## License
|
|
281
340
|
|
|
282
|
-
|
|
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.
|