@lownoise-studio/render-shield-react
v0.4.0
Published
Lightweight React render diff shielding with structured diagnostics.
Readme
RenderShield React A semantic instrumentation layer for React render boundaries.
React can skip rerenders.
But when it does — how do you verify what was actually prevented, and why?
RenderShield React is a lightweight developer instrument that:
Applies structured prop comparison
Allows surgical deep-watching of specific nested paths
Reports why a render was shielded or accepted
Provides pattern-based recommendations (v0.4.0+)
Supports component contracts for explicit documentation (v0.4.0+)
Includes runtime warnings to detect and clarify misuse (v0.4.0+)
It does not mutate props. It does not rewrite state. It does not guarantee performance gains.
It exposes the decision boundary.
It prefers doing nothing over doing the wrong thing.
Why This Exists
Many unnecessary rerenders originate from:
Unstable object references
Inline functions recreated each render
Deep state updates unrelated to rendered output
Parent rerender cascades
AI-generated components that leak references
Blindly applying React.memo, useMemo, or useCallback can obscure the underlying cause.
RenderShield React is not a magic fix.
It is a visibility instrument.
It helps you answer:
Did this render actually need to happen?
Which keys changed?
Were watched paths stable?
Was shielding correct?
You don’t guess.
You verify.
Installation npm install @lownoise-studio/render-shield-react
Understanding the Two APIs
RenderShield React provides two APIs with different semantics:
Higher-Order Component (withRenderShield)
When the comparison says "equal", the wrapped component does not execute. This is true render prevention: React.memo prevents the component function from running. Use when: You want to skip component execution entirely when certain props/paths are equal.
Hook (useRenderShield)
When the comparison says "equal", the hook returns the previous value, but the component still executes. The component function runs every time; only the value passed to children (or used in hooks) is stabilized. Use when: The component must run every render, but you want to stabilize the value passed downstream.
Key Difference
HOC: Prevents component execution (saves CPU in the component itself). Hook: Component always runs (no CPU save in the component); stabilizes output for downstream consumers.
When to Use Which
Use withRenderShield (HOC) when:
- You want to prevent a component from re-executing when props are equal
- The component is expensive to render
- You want the same behavior as React.memo but with structured comparison
Use useRenderShield (Hook) when:
- The component must run every render (e.g., for side effects, logging, or other reasons)
- You want to stabilize the value passed to children or other hooks
- You need diagnostics but don't want to change component execution behavior
Mental Model
HOC → Execution gate
Hook → Reference stabilizer
Report → Semantic analyst
These roles are intentionally separate.
RenderShield does not blur them.
This locks the architecture in people's heads.
Core Hook useRenderShield( value: T, options?: { watch?: string[]; debug?: boolean; visual?: boolean; shield?: boolean; contract?: { watch: string[]; description?: string; }; customCompare?: (prev: T, next: T) => boolean; componentName?: string; } )
Default Behavior (Shallow Comparison)
By default, the hook performs a shallow comparison of top-level keys.
If no top-level keys changed → previous reference is returned.
If any top-level key changed → new value is accepted.
No mutation occurs.
Original references are preserved.
Shallow comparison remains O(n) where n is the number of top-level keys.
No hidden recursion.
Watch Paths (Targeted Deep Comparison)
You may provide specific nested paths to compare:
const shieldedProps = useRenderShield(props, { watch: ["user.id"] });
When watch is provided:
Only those paths are deep-compared.
No full-object recursion occurs.
If watched paths are stable, shielding may occur even if unrelated keys changed.
If a watched path changes, shielding is disabled.
This keeps comparison surgical and intentional.
Staleness Contract
When the hook returns the previous value (shielding occurs), only the watched paths are guaranteed to match the comparison logic you configured.
Important: Any other keys on the returned object may be stale (from the previous render).
Safe Usage Patterns
Only read watched paths from the returned value:
const shieldedProps = useRenderShield(props, { watch: ["user.id"] }); // Safe: user.id is watched const userId = shieldedProps.user.id; // Risk: user.name may be stale if it changed but user.id didn't const userName = shieldedProps.user.name; // May be outdated
Pass the returned value to children that only depend on watched paths:
const shieldedProps = useRenderShield(props, { watch: ["user.id"] }); // Safe: Child only uses user.id return ;
If you need to read non-watched keys, use the original props:
const shieldedProps = useRenderShield(props, { watch: ["user.id"] }); // Read watched path from shielded value const userId = shieldedProps.user.id; // Read non-watched path from original props const userName = props.user.name;
Custom Comparator
You may supply your own comparison logic:
useRenderShield(props, { customCompare: (prev, next) => prev.id === next.id });
If provided:
The custom comparator takes precedence.
Comparison logic remains explicit and user-defined.
No additional heuristics are applied.
Debug Diagnostics
Enable diagnostic logging:
useRenderShield(props, { watch: ["user.id"], debug: true });
Console output includes:
Whether shielding occurred
Render count
Changed keys
Stable keys
Watched path results
Classification severity
Contract compliance status (if contract is provided)
Recommendations (pattern-based, low-noise)
Summary statistics
Logs are disabled in production builds.
Debug mode is strictly for development analysis.
Runtime Warnings (v0.4.0+)
In debug mode, when shielding occurs with non-watched changed keys, RenderShield warns about potential staleness:
⚠️ Staleness Risk: Shielding occurred but non-watched keys changed: [keys]. The returned value may contain stale data for these keys.
When using the hook (not HOC), a one-time clarification appears:
ℹ️ Note: useRenderShield returned a previous value (shielding), but the component still executed. The hook stabilizes the value for downstream consumers but does NOT prevent component rerenders.
These warnings help prevent misuse and clarify behavior.
Diagnostics-Only Mode
For safe experimentation, you can enable diagnostics without changing component behavior:
useRenderShield(props, { watch: ["user.id"], debug: true, shield: false // Always return current value, but still report diagnostics });
When shield: false:
The hook always returns the current value (no shielding behavior).
Diagnostics are still reported when debug: true.
Enables you to see what would happen without actually changing component behavior.
This is useful for:
- Understanding which keys change without risking staleness bugs
- Auditing render behavior before implementing shielding
- Verifying your watch paths are correct
For clarity, you can also use the useRenderShieldReport alias:
import { useRenderShieldReport } from "@lownoise-studio/render-shield-react";
const props = useRenderShieldReport(value, { debug: true, watch: ["user.id"] });
Optional Visual HUD (v0.3+)
You may enable a minimal visual overlay during development:
useRenderShield(props, { watch: ["user.id"], debug: true, visual: true });
When:
debug === true
visual === true
a render was successfully shielded
A small, temporary development HUD toast appears.
Design constraints:
SSR safe (document guard)
No React lifecycle injection
Single shared DOM node
Auto-removal after ~2 seconds
No global state mutation
This is a development instrument — not a UI system.
Higher-Order Component const Shielded = withRenderShield(Component, { watch: ["user.id"], debug: true, contract: { watch: ["user.id"], description: "Component only depends on user ID" } });
The HOC:
Wraps React.memo
Applies the same comparison logic
Does not mutate props
Does not inject state
Does not modify component behavior
It influences rerender decisions only.
Component Contracts (v0.4.0+)
Document which paths matter for your component:
useRenderShield(props, { watch: ["user.id"], contract: { watch: ["user.id"], description: "This component only cares about user ID" }, debug: true });
Contracts enable:
Explicit documentation of component dependencies
Contract compliance reporting (✓ Compliant or ⚠ Drift)
Contract drift detection in recommendations
Makes "what matters" discoverable and verifiable
Pattern-Based Recommendations (v0.4.0+)
RenderShield analyzes render patterns and suggests improvements:
After 3+ renders with the same pattern, recommendations appear:
💡 Recommendations • Consider watching: [frequently changed keys] • Contract specifies [paths] but [other paths] changed
Recommendations are generated in reporting only and never affect comparison logic.
Recommendations are low-noise:
Only appear when patterns are detected (>= 3 occurrences)
Capped to top 3 most repeated keys
Based on actual render history (last 10 renders per component)
Help connect "what changed" → "what matters" → "what to do next"
Severity Classification
When debug mode is enabled, comparisons are classified as:
Stable
Changed (non-UI key)
Changed (watched key)
Custom compare triggered
These classifications are informational.
They do not alter runtime behavior.
Example: The Invisible Cascade
A parent updates user.lastActive every 800ms.
Your component only depends on user.id.
Without structured comparison, rerenders may cascade silently.
With RenderShield:
useRenderShield(props, { watch: ["user.id"], debug: true });
Only user.id is deep-compared.
Unrelated changes are classified and reported.
You don’t guess.
You verify.
What It Is Not
RenderShield React is not:
A compiler
A code transformation tool
A React internals patch
A guaranteed performance fix
A global runtime modifier
A full deep-equality engine by default
It favors clarity over automation.
It prefers explicit control over hidden behavior.
Design Constraints
RenderShield React:
Is React 18+ compatible
Does not rely on experimental APIs
Does not mutate inputs
Does not modify React internals
Does not introduce global side effects
Avoids deep recursion unless explicitly requested
Keeps shallow comparison O(n)
It prefers doing nothing over doing the wrong thing.
Intended Use Cases
RenderShield React works best when:
Diagnosing rerender cascades
Auditing AI-generated components
Verifying React.memo effectiveness
Validating watch-path stability
Building controlled component boundaries
Teaching render mechanics to teams
It is a diagnostic surface.
Not an optimization promise.
Status
v0.4.0 (Current)
Core hook stable
HOC stable
Watch-path targeting validated
Type-safe
Tests passing (14/14)
CJS, ESM, and DTS builds
New in v0.4.0:
Pattern-based recommendations (low-noise, actionable)
Component contracts (explicit "what matters" documentation)
Enhanced console output (summary statistics, contract compliance)
Runtime warnings (staleness risk, hook/HOC clarification)
Diagnostics-only mode (safe experimentation)
v0.3.x: Optional visual development HUD support
v0.2.x: Core functionality and watch paths
Feature Freeze & Feedback Cycles
v0.4.0 represents a stable feature set focused on developer experience and safety.
We are freezing feature development to gather real-world feedback.
Focus areas for feedback:
How recommendations help (or don't) in practice
Contract usage patterns and effectiveness
Warning clarity and usefulness
Integration with existing workflows
Performance impact in production applications
Please share feedback via GitHub Issues or discussions.
The core remains intentionally conservative.
Future versions will prioritize stability and real-world validation over new features.
License
MIT
