Skip to content

behroodrzdr/react-smart-render-validator

Repository files navigation

react-smart-render-validator

Development-only diagnostics tool to detect, explain, and help prevent unnecessary React component re-renders.


Table of contents

  1. Overview
  2. Why it exists
  3. Features
  4. Install
  5. Quick start
  6. APIs
  7. Configuration options
  8. Examples
  9. Dev overlay / DevTools panel
  10. Performance considerations
  11. Limitations and caveats
  12. Testing
  13. Roadmap & extension ideas
  14. Contributing
  15. License

Overview

react-smart-render-validator is a lightweight development-only utility that helps React developers detect and understand unnecessary re-renders and prop/state changes.

It focuses on giving actionable insights during development by:

  • Showing which props or parts of state changed between renders.
  • Classifying changes as reference-only (shallow diff but deep equal) vs value changes.
  • Suggesting remedial actions (e.g. useMemo, useCallback, React.memo).
  • Providing an optional overlay and DevTools panel to visualize hot components.

Important: This package is intended only for development. It is safe by design to disable automatically in production builds.


Why it exists

Large React applications frequently suffer from performance regressions caused by unnecessary re-renders. Identifying those re-renders and understanding why they happened (which prop changed? which nested key?) is time consuming with current tooling. This package fills that gap by offering actionable diagnostics, recording, and developer-friendly suggestions.


Features

  • Wrap components (HOC) or use a hook to enable diffs.
  • Deep vs shallow diff classification per-prop.
  • Console warnings with structured diagnostics (previous value, next value, diff type).
  • Optional browser overlay highlighting components that render too often.
  • Configurable thresholds and filters (whitelist/blacklist by name or file path).
  • TypeScript support and small runtime footprint in dev mode.
  • Integration helpers for React.memo, useCallback, and useMemo suggestions.

Install

Install from npm (development dependency recommended):

# with npm
npm install --save-dev react-smart-render-validator

# with yarn
yarn add -D react-smart-render-validator

# with pnpm
pnpm add -D react-smart-render-validator

Note: Add as a devDependency to avoid shipping it to production builds.


Quick start

Wrap your app with SmartRenderProvider in development only, then either wrap components with withSmartValidator or use the useSmartValidator hook.

import React from 'react';
import { SmartRenderProvider, withSmartValidator } from 'react-smart-render-validator';
import App from './App';

const DevApp = () => (
  <SmartRenderProvider config={{ enabled: process.env.NODE_ENV === 'development' }}>
    <App />
  </SmartRenderProvider>
);

export default DevApp;

Then wrap a component:

import { withSmartValidator } from 'react-smart-render-validator';

function UserCard({ user, onClick }) {
  return <div onClick={onClick}>{user.name}</div>;
}

export default withSmartValidator(UserCard, { checkDeepEqualProps: ['user'] });

When UserCard renders, the package will print a structured message to the console describing which props changed and whether those changes were reference-only.


APIs

SmartRenderProvider

Top-level provider. Must wrap the portion of your app you want to observe. Accepts global config.

<SmartRenderProvider config?: SmartRenderConfig>
  {children}
</SmartRenderProvider>

SmartRenderConfig (defaults shown):

interface SmartRenderConfig {
  enabled: boolean; // default: process.env.NODE_ENV === 'development'
  showOverlay?: boolean; // default: false
  threshold?: number; // number of renders per 5s before flagged; default: 10
  deepCheckDefault?: boolean; // default: false (shallow by default, deep if configured per-prop)
  stackTraceOnWarn?: boolean; // default: false
  whitelist?: Array<string | RegExp>; // component names to always monitor
  blacklist?: Array<string | RegExp>; // component names or file paths to ignore
  output?: 'console' | 'overlay' | 'both'; // default: 'console'
}

withSmartValidator(Component, options?)

Higher-order component wrapper. Prefer this for class components or to annotate named components.

withSmartValidator<P>(Component: React.ComponentType<P>, options?: WithSmartOptions): React.FC<P>

WithSmartOptions:

interface WithSmartOptions {
  name?: string; // override component display name
  checkDeepEqualProps?: string[]; // keys in props to deep-compare
  ignoreProps?: string[]; // keys to ignore
  warnIfShallowOnly?: boolean; // warn when shallow change but deepEqual
  threshold?: number; // override provider threshold
}

useSmartValidator(refOrName, props, options?)

Hook-based API for functional components. Call it inside the component body.

useSmartValidator(componentNameOrRef: string | React.RefObject, props: any, options?: UseSmartOptions)

UseSmartOptions is similar to WithSmartOptions.

Example

function TodoItem(props) {
  useSmartValidator('TodoItem', props, { checkDeepEqualProps: ['item'] });
  return <div>{props.item.title}</div>;
}

useRenderLogger() (Optional)

A small hook that returns a render count and last diffs; useful for unit tests or custom logging.

const { renders, lastDiff } = useRenderLogger();

Dev overlay & CLI

  • start-overlay (dev-only) launches an in-browser overlay that shows hot components, render frequency charts, and clickable entries to view prop diffs.

  • CLI command (for local dev) to snapshot current render metrics and save to a file.

npx react-smart-render-validator snapshot --output=render-report.json

Configuration options (detailed)

  • enabled (boolean): globally enable the validator. Default: process.env.NODE_ENV === 'development'.
  • showOverlay (boolean): mount the overlay in the DOM to visually inspect render hot-spots.
  • threshold (number): number of renders within a monitoring window (5 seconds) considered suspicious.
  • deepCheckDefault (boolean): whether to run deep equality checks by default. Deep checks are more expensive; prefer per-prop configuration.
  • stackTraceOnWarn (boolean): include stack trace in console warnings so you can trace the cause.
  • whitelist / blacklist: array of strings or regular expressions matching component display names or file paths.
  • output: 'console' | 'overlay' | 'both' — where to send diagnostics.
  • allowedModulesForDeepCheck: optional list of package/module names that are safe to deep-compare (helps avoid expensive checks on large objects like React elements).

Per-component options (HOC/hook)

  • checkDeepEqualProps: array of prop keys to deep-compare.
  • ignoreProps: array of keys to ignore while diffing (e.g., children, theme)
  • warnIfShallowOnly: log if shallow changed but deep equal — this signals reference churn.
  • label: custom label used in overlay and logs.

Examples

Basic usage (functional component)

import React from 'react';
import { SmartRenderProvider, useSmartValidator } from 'react-smart-render-validator';

function Counter({ value, onInc }) {
  useSmartValidator('Counter', { value, onInc }, { checkDeepEqualProps: [] });
  return (
    <div>
      <span>{value}</span>
      <button onClick={onInc}>+1</button>
    </div>
  );
}

export default function App() {
  const [n, setN] = React.useState(0);
  return (
    <SmartRenderProvider config={{ enabled: true }}>
      <Counter value={n} onInc={() => setN(n+1)} />
    </SmartRenderProvider>
  );
}

When Counter renders you will get console output like:

[SmartRender] Counter rendered (2 -> 3). Props diff:
 - value: 1 -> 2 (primitive change)
 - onInc: function -> function (same content but new reference). Suggest useCallback

Using withSmartValidator (class or plain wrap)

import { withSmartValidator } from 'react-smart-render-validator';

class LargeList extends React.PureComponent {
  render() {
    return <ul>{this.props.items.map(i => <li key={i.id}>{i.text}</li>)}</ul>;
  }
}

export default withSmartValidator(LargeList, { checkDeepEqualProps: ['items'], warnIfShallowOnly: true });

This will flag when items prop becomes a new array reference but is deep-equal.

TypeScript example

import React from 'react';
import { withSmartValidator, SmartRenderProvider } from 'react-smart-render-validator';

interface User { id: number; name: string }
interface UserCardProps { user: User; onClick?: () => void }

function UserCard({ user, onClick }: UserCardProps) {
  return <div onClick={onClick}>{user.name}</div>;
}

export default withSmartValidator<UserCardProps>(UserCard, { checkDeepEqualProps: ['user'] });

Type definitions are bundled and should provide autocompletion for options.

Integration with React.memo, useCallback

The validator will print suggestions when it detects function props changing identity every render:

[SmartRender] MyComp rendered. Prop `onClose` changed reference on every render. Suggest: const onClose = useCallback(() => ..., [deps])

It will also point out when React.memo could help, for pure presentational components.


Dev overlay / DevTools panel

When showOverlay is enabled, the overlay mounts as a small panel in the bottom-right of the app and shows:

  • Sorted list of monitored components by render frequency.
  • Click a component to view recent prop diffs.
  • A small sparkline for each component showing render frequency over time.
  • Toggle to freeze or clear metrics.

Security note: The overlay does not send any data to external services. All metrics are local to the browser session.


Performance considerations

The package is explicitly meant for development: expensive checks are opt-in. Guidelines:

  • Install as a devDependency and gate with process.env.NODE_ENV !== 'production' or enabled flag.
  • Prefer per-prop deep checks (checkDeepEqualProps) rather than global deep checks.
  • The overlay and additional telemetry incur extra overhead; disable overlay in large apps when not needed.
  • The internal sampling window is configurable — increase it to reduce overhead.

Implementation notes for maintainers:

  • Use WeakMap to store previous props by component instance to avoid memory leaks.
  • Use a shallow comparison first and only run fast-deep-equal (or similar) when shallow indicates difference and deep checks are requested.

Limitations and caveats

  • This tool cannot (and will not) reliably detect rendering caused by external factors like global CSS changes or layout reflows.
  • Deep-equality is an approximation; circular structures or extremely large objects may be skipped.
  • The overlay relies on requestAnimationFrame and timers — in heavily throttled or headless environments (CI), behavior may differ.
  • Because it hooks into renders, infinite loops in user code will generate a lot of records — the package tries to be resilient but will not stop app execution.

Testing

  • Unit tests should mock renders with @testing-library/react and assert console output by spying console.warn/console.group.
  • Integration tests for the overlay can use jsdom snapshot testing and Playwright for visual checks.
  • Add tests that assert withSmartValidator does not alter component behavior in production mode.

Example Jest test snippet:

import React from 'react';
import { render } from '@testing-library/react';
import { SmartRenderProvider, withSmartValidator } from 'react-smart-render-validator';

it('logs shallow-only prop changes when configured', () => {
  const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
  function C({ items }) { return <div>{items.length}</div> }
  const W = withSmartValidator(C, { checkDeepEqualProps: ['items'], warnIfShallowOnly: true });
  const { rerender } = render(
    <SmartRenderProvider config={{ enabled: true }}>
      <W items={[1,2,3]} />
    </SmartRenderProvider>
  );

  rerender(
    <SmartRenderProvider config={{ enabled: true }}>
      <W items={[1,2,3]} />
    </SmartRenderProvider>
  );

  expect(spy).toHaveBeenCalled();
  spy.mockRestore();
});

Roadmap & extension ideas

  • Auto-fix suggestions: CLI tool that scans repo and suggests useCallback or React.memo placements (manual review before applying).
  • CI mode: Fail builds when new unnecessary render hotspots are introduced (threshold-based).
  • Editor plugin: VSCode extension that surfaces hotspots inline while editing.
  • Server-side collection: Optionally collect anonymized metrics in CI to detect regressions across PRs (opt-in, privacy-aware).
  • Better integrations: Official integrations for Storybook and Next.js.

Contributing

Contributions welcome!

  • Fork the repo and open a PR.
  • Follow the code style (eslint, prettier), add tests for new behavior, and update docs.
  • We use a main branch and semantic-release for releases.

Suggested labels for issues: bug, feature, perf, docs.


License

MIT


Appendix: Internals (implementation sketch)

A concise description of a possible internal implementation to help contributors:

  1. Component identity: For HOC-wrapped components, create a per-instance id and store previous props in a WeakMap(instance -> prevProps).

  2. Diffing pipeline:

    • Run shallowEqual(prevProps, nextProps) first.
    • For changed keys, if key is configured for deep check, run fast-deep-equal.
    • Build a structured Diff object: { key, type: 'primitive'|'object'|'function'|'ref-only'|'deep-equal', prev, next }.
  3. Telemetry & thresholds: Maintain an in-memory circular buffer of render timestamps per-component to compute render frequency.

  4. Logging: Use grouped console logs for readability. Optionally emit a structured window.__SMART_RENDER__ object for external tooling.

  5. Overlay: Mount an isolated shadow-root DOM node to avoid style collisions and render metrics; use a small React app for the panel.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published