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

prop-ghost

v0.0.1

Published

Detects props you pass to React components but never use — runtime detection with visual overlay and enterprise-grade security

Readme

prop-ghost 👻

Detects props you pass to React components but never use — runtime detection for dead prop passing.

npm version Bundle size License: MIT


The Problem

ESLint catches unused variables inside a component. But nobody catches this:

// Parent passes isAdmin, highlight, theme, onClose
<Card isAdmin={true} highlight={false} theme="dark" onClose={handleClose} />

// Inside Card — only title and children are ever used
function Card({ title, children }) {
  return <div>{title}{children}</div>;
}

isAdmin, highlight, theme, onClose are ghost props — silently ignored, never causing an error, polluting your codebase for months.


Installation

npm install --save-dev prop-ghost
yarn add --dev prop-ghost
pnpm add --save-dev prop-ghost

Quick Start

Option 1: HOC (Recommended)

Wrap your component with withGhostDetector:

import { withGhostDetector } from 'prop-ghost';

function Card({ title, children }) {
  return <div><h1>{title}</h1>{children}</div>;
}

// Wrap in development
const TrackedCard = withGhostDetector(Card);

// Usage
<TrackedCard title="Hello" unusedProp="ghost" />
// ⚠️ [prop-ghost] Card received 1 unused prop: ["unusedProp"]

Option 2: Global Provider

Wrap your app to configure ghost detection globally:

import { GhostProvider } from 'prop-ghost';

// In your app root (development only)
if (process.env.NODE_ENV === 'development') {
  root.render(
    <GhostProvider config={{ ignore: ['className', 'data-*'] }}>
      <App />
    </GhostProvider>
  );
}

Features

Runtime detection — Works with any React setup, no build tools required ✅ 👻 Visual overlay — Real-time reporting with search, filter, and export ✅ 🔒 Enterprise security — Value sanitization, prop size limits, rate limiting, safe mode ✅ Zero dependencies — Only peer dependency: React >=17 ✅ Tiny bundle — < 3.9KB gzipped (HOC + overlay), < 2.4KB (HOC only) ✅ TypeScript — Full type safety ✅ Dev-only — Completely tree-shaken in production builds ✅ Framework agnostic — Works with Next.js, Vite, CRA, Remix, etc.


API Reference

withGhostDetector(Component, options?)

Higher-Order Component that wraps a component to detect unused props.

import { withGhostDetector } from 'prop-ghost';

const TrackedCard = withGhostDetector(Card);
const TrackedCardWithOptions = withGhostDetector(Card, {
  displayName: 'CustomCard',
  config: {
    ignore: ['className', 'style'],
    trackEveryRender: false,
  },
});

Options:

interface GhostDetectorOptions {
  displayName?: string;           // Override component name in reports
  config?: Partial<GhostConfig>;  // Per-component config
}

<GhostProvider>

Context provider for global configuration.

import { GhostProvider } from 'prop-ghost';

<GhostProvider config={config}>
  <App />
</GhostProvider>

Config:

interface GhostConfig {
  // Props to ignore (exact match, glob pattern, or regex)
  ignore?: (string | RegExp)[];

  // Report on every render (default: false, reports on unmount only)
  trackEveryRender?: boolean;

  // Custom reporter function
  onGhostPropsDetected?: (report: GhostReport) => void;

  // Enable visual overlay (v2 feature)
  enableOverlay?: boolean;

  // 🔒 Security options (all enabled by default)

  // Sanitize prop values in reports (default: true)
  sanitizeValues?: boolean;

  // Prop names to always mask (default: ['password', 'token', 'apiKey', 'secret', ...])
  sanitizePropNames?: string[];

  // Maximum prop size in bytes (default: 102400 = 100KB)
  maxPropSize?: number;

  // Throttle duplicate reports in ms (default: 5000)
  reportThrottle?: number;

  // Catch all errors, never throw (default: true)
  safeMode?: boolean;
}

Examples:

// Ignore design system pass-through props
<GhostProvider config={{ ignore: ['className', 'style', 'data-*', /^aria-/] }}>
  <App />
</GhostProvider>

// Custom reporter
<GhostProvider config={{
  onGhostPropsDetected: (report) => {
    console.error(`Ghost props in ${report.componentName}:`, report.ghostProps);
    // Send to analytics, log to file, etc.
  }
}}>
  <App />
</GhostProvider>

useGhostProps(props, component?, options?)

Hook for manual prop tracking.

import { useGhostProps } from 'prop-ghost';

function Card(props) {
  useGhostProps(props, Card);
  const { title, children } = props;
  return <div>{title}{children}</div>;
}

⚠️ Limitation: For v1 (runtime), this hook has limited functionality because it cannot intercept destructured prop access. It's designed for v2 (Babel auto-injection). For v1, use withGhostDetector HOC instead.


<GhostOverlay>

Visual overlay component that displays ghost prop reports in real-time.

import { GhostOverlay } from 'prop-ghost';

function App() {
  return (
    <>
      <YourApp />
      <GhostOverlay
        position="bottom-right"
        maxReports={10}
        startMinimized={false}
      />
    </>
  );
}

Props:

interface GhostOverlayConfig {
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
  maxReports?: number;           // Default: 10
  startMinimized?: boolean;      // Default: false
  zIndex?: number;               // Default: 9999
}

Features:

  • 📍 Draggable — Click and drag to reposition
  • 🗜️ Minimizable — Collapse to save space
  • 📋 Click to copy — Click any prop name to copy it
  • 📊 Report aggregation — Deduplicates repeated reports with count badge
  • ⏱️ Timestamps — Shows "just now", "5s ago", etc.
  • 🔍 Search — Filter reports by component name or prop name
  • 🎯 Filter — Filter by specific component
  • 📥 Export to JSON — Download all reports with full details
  • 🧹 Clear button — Clear all reports

Example with Provider:

import { GhostProvider, GhostOverlay, withGhostDetector } from 'prop-ghost';

const TrackedCard = withGhostDetector(Card);

function App() {
  return (
    <GhostProvider config={{ ignore: ['className'] }}>
      <TrackedCard title="Test" unusedProp="ghost" />
      <GhostOverlay />
    </GhostProvider>
  );
}

GhostReport

Report format passed to custom reporters:

interface GhostReport {
  componentName: string;      // "Card"
  ghostProps: string[];       // ["isAdmin", "highlight"]
  allProps: string[];         // ["title", "isAdmin", "highlight"]
  usedProps: string[];        // ["title"]
  timestamp: number;          // Date.now()
  instanceId: string;         // Unique per-instance ID
}

Configuration Examples

Ignore Design System Props

withGhostDetector(Card, {
  config: {
    ignore: ['className', 'style', 'data-*', /^aria-/],
  },
});

Track Every Render (Noisy!)

withGhostDetector(Card, {
  config: {
    trackEveryRender: true, // Reports after every render, not just unmount
  },
});

Custom Reporter

withGhostDetector(Card, {
  config: {
    onGhostPropsDetected: (report) => {
      // Send to analytics
      analytics.track('ghost_props_detected', {
        component: report.componentName,
        count: report.ghostProps.length,
        props: report.ghostProps,
      });
    },
  },
});

🔒 Security Features

prop-ghost includes enterprise-grade security measures to prevent sensitive data leakage and ensure safe operation in all environments.

1. Value Sanitization (Enabled by Default)

Automatically sanitizes prop values in reports to prevent leaking sensitive data.

// Instead of showing actual values
{ password: 'secret123', count: 42 }

// Reports show type information
{ password: '[REDACTED]', count: '[number]' }

Sensitive prop names (automatically redacted):

  • password, token, apiKey, secret
  • accessToken, refreshToken
  • Any prop name containing these keywords (case-insensitive)

Configure sanitization:

<GhostProvider config={{
  sanitizeValues: true,  // Default: true
  sanitizePropNames: ['password', 'token', 'apiKey', 'secret', 'ssn'],
}}>
  <App />
</GhostProvider>

2. Prop Size Limits

Prevents memory issues and log spam from huge prop values.

<GhostProvider config={{
  maxPropSize: 102400,  // Default: 100KB
}}>
  <App />
</GhostProvider>

Props larger than the limit are:

  • ✅ Still passed through to the component
  • ✅ Logged as a warning in dev
  • ❌ NOT tracked for ghost detection (to prevent memory bloat)

3. Rate Limiting

Prevents console spam from components that re-render frequently.

<GhostProvider config={{
  reportThrottle: 5000,  // Default: 5000ms (5 seconds)
}}>
  <App />
</GhostProvider>

How it works:

  • Same component + same ghost props → Only reported once per throttle period
  • Different ghost props → Reported immediately (not throttled)
  • Set to 0 to disable throttling

4. Safe Mode (Enabled by Default)

Catches all errors in ghost detection to ensure it never crashes your app.

<GhostProvider config={{
  safeMode: true,  // Default: true
}}>
  <App />
</GhostProvider>

What safe mode protects:

  • Proxy trap failures
  • Report generation errors
  • Custom reporter exceptions
  • Value serialization failures

When safe mode catches an error:

  • ✅ App continues running normally
  • ✅ Error logged in development
  • ✅ Silent in production

Disable for debugging:

<GhostProvider config={{
  safeMode: false,  // Throw errors instead of catching them
}}>
  <App />
</GhostProvider>

How It Works

ES Proxy-Based Tracking

prop-ghost wraps your component's props in an ES Proxy to track which props are accessed during render:

const proxy = new Proxy(props, {
  get(target, prop) {
    tracked.accessed.add(prop);  // Track access
    return target[prop];
  },
  ownKeys(target) {
    tracked.spreadAccessed = true;  // Detect {...props}
    return Reflect.ownKeys(target);
  },
});

Report Timing

  • Default: Reports on component unmount (captures full lifecycle)
  • Opt-in: Reports on every render via trackEveryRender: true

Ignored Props

Always ignored (React internals):

  • children
  • key
  • ref
  • $$typeof

User-configurable via ignore option:

  • Exact match: 'className'
  • Glob pattern: 'data-*'
  • Regex: /^aria-/

Edge Cases & Limitations

1. Spread Operator

When {...props} is used, the Proxy sees it as a single access. All props are marked as "potentially used" to avoid false positives.

function Card({ title, ...rest }) {
  return <div {...rest}><h1>{title}</h1></div>;
}

// No warnings — spread operator marks all props as used
<Card title="Test" className="card" onClick={() => {}} />

2. Conditional Props

Props used only in certain conditions may be falsely reported as ghost props:

function Card({ title, errorMessage }) {
  const [hasError, setHasError] = useState(false);
  return hasError ? <div>{errorMessage}</div> : <div>{title}</div>;
}

// If hasError is never true, errorMessage is reported as ghost
<Card title="Test" errorMessage="Error occurred" />

Workaround: Use trackEveryRender: true to catch dynamic usage.

3. Component Display Names

Priority order for component names in reports:

  1. component.displayName (explicit)
  2. component.name (function name)
  3. Parsed from Error stack trace (unreliable)
  4. Fallback: <Anonymous>

Set displayName explicitly for better reports:

Card.displayName = 'Card';

Performance

  • Proxy overhead: ~0.1ms per component render (negligible)
  • Dev-only: Entire library is tree-shaken in production builds
  • Memory: ~1KB per tracked component (Set of prop names)

TypeScript Support

Full TypeScript support with type inference:

interface CardProps {
  title: string;
  description?: string;
  onClick?: () => void;
}

const Card: React.FC<CardProps> = ({ title, children }) => (
  <div><h1>{title}</h1>{children}</div>
);

// Types are preserved through the HOC
const TrackedCard = withGhostDetector(Card);

// ✅ Type-safe
<TrackedCard title="Test">Content</TrackedCard>

// ❌ Type error
<TrackedCard invalid="prop" />

React Version Compatibility

  • ✅ React 17.0.0+
  • ✅ React 18.x
  • ✅ React 19.x

Tested against all major versions. Uses only public React APIs (no internals).


Framework Support

Works with all React frameworks:

  • ✅ Next.js (App Router & Pages Router)
  • ✅ Vite
  • ✅ Create React App
  • ✅ Remix
  • ✅ Gatsby
  • ✅ Parcel
  • ✅ Webpack

Roadmap

v0.3.0 (Current)

  • [x] Runtime detection via HOC
  • [x] Global config via Provider
  • [x] TypeScript support
  • [x] 🔒 Enterprise security features
    • [x] Value sanitization
    • [x] Prop size limits
    • [x] Rate limiting
    • [x] Safe mode
  • [x] 👻 Visual overlay UI
    • [x] Draggable panel
    • [x] Report aggregation
    • [x] Click to copy props
    • [x] Minimize/maximize
    • [x] Timestamps
    • [x] Search by component/prop name
    • [x] Filter by component
    • [x] Export to JSON
  • [x] < 3.9KB bundle size (HOC + overlay)
  • [x] Example demo app

v1.0.0 (Next)

  • [ ] Production testing
  • [ ] Additional test coverage
  • [ ] Community feedback integration

v2.0.0 (Planned)

  • [ ] Babel plugin for static analysis
  • [ ] Auto-inject useGhostProps at compile time
  • [ ] Cross-file prop tracking
  • [ ] JSON report output

v2.1.0 (Future)

  • [ ] IDE integration (VS Code extension)
  • [ ] ESLint rule (static-only alternative)

Contributing

Contributions welcome! Please open an issue or PR.

Development

# Install dependencies
npm install

# Run tests
npm run test

# Build library
npm run build

# Check bundle size
npm run size

License

MIT © Vidhya Sagar Thakur


Acknowledgments

Inspired by:

  • React DevTools component name extraction
  • react-time-warp (sibling project)
  • The eternal struggle against dead code

FAQ

Q: Does this work in production?

A: No. The entire library is tree-shaken in production builds via process.env.NODE_ENV === 'production' guards. Zero runtime cost.

Q: Why not use TypeScript for this?

A: TypeScript catches type mismatches, not unused-but-valid props. If a prop is defined in the interface but never used, TypeScript won't complain.

Q: What about ESLint's react/no-unused-prop-types?

A: That rule only works with prop-types (deprecated). It doesn't work with TypeScript interfaces or modern React.

Q: Does this slow down my app?

A: Negligibly (~0.1ms per component), and only in development. Completely removed in production.

Q: Can I use this with class components?

A: Yes, wrap them with withGhostDetector. However, the HOC is designed for function components. Class component support is not a priority.

Q: Does this work with React.memo / forwardRef?

A: Yes. The HOC properly forwards refs and preserves memoization.


Star this repo if prop-ghost helped you!