why-render-react
v1.0.1
Published
A React hook that helps you understand why a component re-rendered.
Readme
why-render-react
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).
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.
Features
- Zero-config logging of re-render causes, per component
- Shows exactly which props changed
- Optional
verbosemode with previous/next values - Per-component
isActivetoggle to silence noisy components without disabling everything - One-line global
setup()— works in dev, can be disabled in production
Installation
npm install why-render-reactSetup
Call setup() once, before your app renders:
// index.tsx
import { setup } from "why-render-react"
import { createRoot } from "react-dom/client"
import App from "./App"
setup(true)
createRoot(document.getElementById("root")!).render(<App />)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.
setup(process.env.NODE_ENV !== "production")Basic Usage
Call useWhyRerender inside any component you want to watch, passing a name and the props you want tracked:
// CarCard.tsx
import { useWhyRerender } from "why-render-react"
interface CarCardProps {
color: string
}
function CarCard(props: CarCardProps) {
useWhyRerender({ name: "CarCard", props })
return <div>{props.color}</div>
}
export default CarCardExamples
1. First render
Nothing to compare yet, so it just logs the mount.
import { useWhyRerender } from "why-render-react"
interface CarCardProps {
color: string
}
function CarCard(props: CarCardProps) {
useWhyRerender({ name: "CarCard", props })
return <div>{props.color}</div>
}
export default CarCardOutput:
[why-rerender] <CarCard> render #1 → initial mount2. A prop actually changes
import { useState } from "react"
import { useWhyRerender } from "why-render-react"
function CarCard({ color }: { color: string }) {
useWhyRerender({ name: "CarCard", props: { color } })
return <div>{color}</div>
}
function Garage() {
const [color, setColor] = useState("blue")
return (
<>
<button onClick={() => setColor("red")}>Repaint</button>
<CarCard color={color} />
</>
)
}
export default GarageOutput (click "Repaint" — the line expands to show details):
▶ [why-rerender] <CarCard> render #3 → 1 prop changed
CHANGED color3. Re-render with no prop changes
The parent re-renders (e.g. from unrelated state), but the child's props never change.
import { useState } from "react"
import { useWhyRerender } from "why-render-react"
function CarCard({ color }: { color: string }) {
useWhyRerender({ name: "CarCard", props: { color } })
return <div>{color}</div>
}
function Garage() {
const [, forceTick] = useState(0)
return (
<>
{/* Re-renders Garage (and CarCard) on every click, but color never changes */}
<button onClick={() => forceTick((t) => t + 1)}>Refresh</button>
<CarCard color="blue" />
</>
)
}
export default GarageOutput:
[why-rerender] <CarCard> render #4 → no prop changes — check state, context, or parent re-render4. Verbose mode
Set verbose: true to see the previous and next values alongside each changed prop.
import { useState } from "react"
import { useWhyRerender } from "why-render-react"
function CarCard({ color }: { color: string }) {
useWhyRerender({ name: "CarCard", props: { color }, verbose: true })
return <div>{color}</div>
}
function Garage() {
const [color, setColor] = useState("blue")
return (
<>
<button onClick={() => setColor("red")}>Repaint</button>
<CarCard color={color} />
</>
)
}
export default GarageOutput:
▶ [why-rerender] <CarCard> render #3 → 1 prop changed
CHANGED color { prev: "blue", next: "red" }5. Silencing one noisy component
If a single component logs too often to debug around, set isActive: false on that one instead of disabling setup() for the whole app.
import { useWhyRerender } from "why-render-react"
interface NoisyWidgetProps {
cursorX: number
cursorY: number
}
function NoisyWidget(props: NoisyWidgetProps) {
useWhyRerender({ name: "NoisyWidget", props, isActive: false })
return (
<div>
{props.cursorX},{props.cursorY}
</div>
)
}
function Sidebar(props: { title: string }) {
useWhyRerender({ name: "Sidebar", props })
return <aside>{props.title}</aside>
}
function Dashboard(props: { title: string }) {
useWhyRerender({ name: "Dashboard", props })
return (
<>
<Sidebar title={props.title} /> {/* still logs */}
<NoisyWidget cursorX={0} cursorY={0} /> {/* silenced */}
</>
)
}
export default Dashboard6. The hidden cause: a new function every render
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.
import { useState } from "react"
import { useWhyRerender } from "why-render-react"
interface Car {
id: number
color: string
}
interface CarCardProps {
id: number
color: string
onSelect: (id: number) => void
}
function CarCard({ id, color, onSelect }: CarCardProps) {
useWhyRerender({
name: "CarCard",
props: { id, color, onSelect },
verbose: true,
})
return <div onClick={() => onSelect(id)}>{color}</div>
}
function CarList({ cars }: { cars: Car[] }) {
const [selectedId, setSelectedId] = useState<number | null>(null)
const setId = (id: number) => {
if (selectedId !== id) {
setSelectedId(id)
}
}
return (
<>
{cars.map((car) => (
<CarCard
key={car.id}
id={car.id}
color={car.color}
// New function every render -> onSelect always shows as "changed"
onSelect={(id) => setId(id)}
/>
))}
</>
)
}
export default CarListOutput:
▶ [why-rerender] <CarCard> render #2 → 1 prop changed
CHANGED onSelect { prev: "ƒ", next: "ƒ" }The fix — memoize the handler with useCallback so the same function reference is reused across renders:
import { useState, useCallback } from "react"
import { useWhyRerender } from "why-render-react"
function CarList({ cars }: { cars: Car[] }) {
const [selectedId, setSelectedId] = useState<number | null>(null)
const setId = useCallback((id: number) => {
setSelectedId((current) => (current !== id ? id : current))
}, [])
return (
<>
{cars.map((car) => (
<CarCard key={car.id} id={car.id} color={car.color} onSelect={setId} />
))}
</>
)
}API Reference
setup(enabled: boolean)
Enables or disables all Why Render logging globally. Must be called once, before your app renders.
useWhyRerender(options)
| Option | Type | Required | Default | Description |
|-----------|-----------------------|----------|---------|-------------------------------------------------------------------------------|
| name | string | Yes | — | Label shown in the console log for this component. |
| props | object | Yes | — | The props (or any values) to track for changes. |
| isActive| boolean | No | true | Set to false to silence logging for just this component. |
| verbose | boolean | No | false | Includes previous/next values for each changed prop in the log. |
Troubleshooting
Error: "WhyRenderConfig has not been configured"
setup() hasn't run yet. Call it once, before your app renders (see Setup).
Nothing is logging, and no error was thrown Check the following:
setup()was actually called with a value that resolves totrue—setup(false), a missing environment variable, or a typo like"tru"will silently disable it.- That component's call doesn't have
isActive: falseset (see Example 5). - If one component is too noisy to debug around, set
isActive: falseon that component instead of disablingsetup()for the whole app.
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). Since these are different values in JavaScript even when their contents look identical, Why Render correctly reports them as changed.
License
MIT License
Copyright (c) why-render-react contributors
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
