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
Maintainers
Readme
prop-ghost 👻
Detects props you pass to React components but never use — runtime detection for dead prop passing.
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-ghostyarn add --dev prop-ghostpnpm add --save-dev prop-ghostQuick 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,secretaccessToken,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
0to 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):
childrenkeyref$$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:
component.displayName(explicit)component.name(function name)- Parsed from Error stack trace (unreliable)
- 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
useGhostPropsat 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 sizeLicense
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! ⭐
