npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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 verbose mode with previous/next values
  • Per-component isActive toggle to silence noisy components without disabling everything
  • One-line global setup() — works in dev, can be disabled in production

Installation

npm install why-render-react

Setup

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 CarCard

Examples

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 CarCard

Output:

[why-rerender] <CarCard> render #1 → initial mount

2. 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 Garage

Output (click "Repaint" — the line expands to show details):

▶ [why-rerender] <CarCard> render #3 → 1 prop changed
    CHANGED   color

3. 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 Garage

Output:

[why-rerender] <CarCard> render #4 → no prop changes — check state, context, or parent re-render

4. 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 Garage

Output:

▶ [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 Dashboard

6. 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 CarList

Output:

▶ [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 to truesetup(false), a missing environment variable, or a typo like "tru" will silently disable it.
  • That component's call doesn't have isActive: false set (see Example 5).
  • If one component is too noisy to debug around, set isActive: false on that component instead of disabling setup() 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.