This document describes the current architecture of virtual-react-json-diff and outlines future directions for the project.
virtual-react-json-diff is a high-performance React component for comparing and visualizing differences between large JSON objects. It leverages virtualized rendering to handle tens of thousands of lines without freezing the UI.
Key responsibilities:
- Generate diff data from two JSON objects
- Transform diff data into a virtualized view
- Provide search, change-block navigation, and statistics
- Optional review/merge mode (accept/reject hunks → merged JSON)
- Imperative ref API (
scrollToPath, expand/collapse, accept/reject all) - Handle user interaction (expand/collapse, scrolling, minimap, keyboard)
What it is not:
- A generic JSON diff algorithm (delegated to
json-diff-kit) - A standalone CLI tool
- A framework-agnostic library (currently React-only)
- A full VCS-style 3-way merge or patch-file generator
| Layer | Technology |
|---|---|
| Diff Engine | json-diff-kit |
| Virtualization | react-window (VariableSizeList) |
| UI Framework | React 18+ |
| Build System | Rollup |
VirtualizedDiffViewer (forwardRef → VirtualDiffViewerRef)
├── SearchboxHolder
├── LineCountDisplay
├── ObjectCountDisplay
├── VirtualDiffGrid
│ └── react-window (VariableSizeList)
│ └── Row Renderer
│ ├── Review gutter (accept/reject) when reviewMode
│ └── Left / Right panes
└── DiffMinimap (x2; padded when reviewMode)
src/
├── index.ts # Public API (viewer, Differ, ChangeBlock, ref types)
└── components/
└── DiffViewer/
├── components/ # UI Components
│ ├── VirtualizedDiffViewer.tsx # Main orchestrator + ref API
│ ├── VirtualDiffGrid.tsx # Virtualized list wrapper
│ ├── DiffMinimap.tsx # Visual navigation
│ └── ...
├── hooks/ # React-specific logic
│ ├── useSearch.ts
│ ├── useRowHeights.ts # Sparse cache keyed by data-index
│ └── ...
├── utils/ # Core orchestration logic
│ ├── preprocessDiff.ts # Segment generation + view building
│ ├── pathAndChangeUtils.ts # Paths, ChangeBlocks, merge JSON
│ ├── diffComparisonOptions.ts # Value normalization
│ ├── lineCountUtils.ts # Statistics
│ ├── objectCountUtils.ts # Object-level stats
│ └── json-diff/
│ ├── row-renderer-grid.tsx # JSX rendering + review UI
│ ├── get-inline-diff.ts # Inline diff logic
│ └── ...
├── types/ # TypeScript definitions
└── __tests__/ # Component + util tests (Bun + happy-dom)
The diff process follows a clear pipeline from raw input to rendered UI:
Location: diffComparisonOptions.ts
If comparisonOptions are provided:
- Ignore specific keys or paths (
ignoreKeys,ignorePaths) - Normalize values based on comparison strategy (
strict,loose,type-aware) - Return preprocessed JSON objects ready for diffing
Example:
const processedOld = preprocessObjectForDiff(oldValue, {
ignoreKeys: ["updatedAt"],
compareStrategy: "type-aware"
});Location: VirtualizedDiffViewer.tsx
- Create or reuse a
Differinstance fromjson-diff-kit - Pass processed objects to
differ.diff(old, new) - Receive raw diff results:
[leftDiff, rightDiff]
Output: Two arrays of DiffResult[] representing line-by-line changes
Location: preprocessDiff.ts → generateSegments()
- Analyze the left diff to identify "equal" vs "changed" blocks
- Split the diff into segments (continuous blocks of equal or changed lines)
- Track segment metadata:
{ start, end, isEqual, isExpanded? }
Purpose: Enable efficient expand/collapse of unchanged regions
Location: preprocessDiff.ts → buildViewFromSegments()
- Walk through segments
- For changed segments: include all lines
- For equal segments:
- If small or expanded: include all lines
- If large and collapsed: include top/bottom context + collapsed marker
- Preserve
originalIndexon each row so later steps can map virtual list rows back to raw diff indices - Generate
DiffRowOrCollapsed[]for both left and right views
Output: Two virtualized views ready for rendering
Location: pathAndChangeUtils.ts
computePaths()— derive JSONPath-like strings per raw diff linegetChangeBlocks()— group consecutive non-equal lines intoChangeBlockhunks (add/remove/modify)- Used by programmatic navigation (
nextChange,scrollToPath, …) and review mode
Location: lineCountUtils.ts, objectCountUtils.ts
- Line stats: Count added/removed/modified lines from raw diff
- Object stats: When using
compare-keyarray diffing, recursively extract object arrays and count changes at the object level
Location: useSearch.ts
- If search is enabled, index text content from both views
- Track line numbers of matches
- Provide navigation functions (
next,previous)
Location: VirtualizedDiffViewer.tsx + pathAndChangeUtils.ts → generateMergedJson()
When reviewMode is enabled:
- Track per-block
ReviewState(pending|accepted|rejected) - On accept/reject (UI, keyboard, or ref API), rebuild a merged JSON by selecting left vs right lines per block
- Notify host via
onReviewChange({ reviewStates, mergedJson }) onReviewChangeis held in a ref so parent callbacks do not re-trigger an update loop
Location: VirtualDiffGrid.tsx → row-renderer-grid.tsx
- Use
react-windowto render only visible rows - Match review/highlight state via
originalIndex(not virtual list index) - For each row:
- Detect row type (
add,remove,modify,equal,collapsed) - Apply syntax highlighting and review status classes
- Render inline diffs for
modifyrows - Render accept/reject controls on the first line of each change block
- Render expand/collapse controls for
collapsedrows
- Detect row type (
Location: DiffMinimap.tsx, useMinimapDraw.ts
- Draw a canvas-based overview of the entire diff
- Highlight changes, search results, and viewport position
- Enable jump-to-location on click
- When
reviewModeis on, overlay gets left padding so minimaps stay aligned with panes
The project is composed of three logical layers, each with distinct responsibilities:
Responsibilities:
- Core diff algorithm (Myers diff, LCS)
- Support for different array comparison methods (
compare-key,lcs,unorder-normal) - Detect circular references
- Provide raw diff output
This library does NOT:
- Generate diffs from scratch
- Implement diffing algorithms
Boundary:
Input: Two JSON objects + DifferOptions
Output: [DiffResult[], DiffResult[]]
Responsibilities:
- Preprocess input objects (ignoreKeys, normalization)
- Segment generation (collapse/expand logic)
- View building from segments
- Path computation and change-block grouping
- Selective merge JSON from review states
- Statistics calculation (line counts, object counts)
- Search and navigation logic
- Row height calculation for virtualization
This layer is mostly framework-agnostic. The logic in utils/ could theoretically be used with Vue, Svelte, or vanilla JS.
Files:
utils/preprocessDiff.tsutils/pathAndChangeUtils.tsutils/diffComparisonOptions.tsutils/lineCountUtils.tsutils/objectCountUtils.tsutils/diffSearchUtils.ts
Boundary:
Input: DiffResult[] + user options (+ optional review states)
Output: Structured view models, change blocks, optional merged JSON
Responsibilities:
- Render JSX for each row type (including review gutter)
- Manage React state and lifecycle (
reviewStates, active change index) - Expose imperative
VirtualDiffViewerRefAPI - Integrate with
react-window - Handle user events (clicks, scrolls, keyboard: j/k, a/r, Enter/Escape)
- Draw canvas-based minimaps
This layer is React-specific.
Files:
components/VirtualizedDiffViewer.tsxcomponents/VirtualDiffGrid.tsxcomponents/DiffMinimap.tsxhooks/useSearch.tshooks/useRowHeights.tsutils/json-diff/row-renderer-grid.tsx
Boundary:
Input: View models + React props
Output: Rendered UI + callbacks / ref methods
A headless library is a UI-agnostic package that provides:
- Core business logic
- State management primitives
- Framework-independent functions
Examples:
@tanstack/table-core(used by React Table, Vue Table, Solid Table)downshift(render props for autocomplete logic)react-hook-formcore logic
Current limitation: The diff orchestration logic (Layer 2) is tightly coupled with React components (Layer 3). This prevents:
- Using the same logic in Vue, Svelte, Angular
- Building CLI tools or Node.js utilities
- Testing logic without React
Benefits of headless architecture:
- Reusability: Core logic can power React, Vue, Svelte adapters
- Testability: Business logic can be tested without React Testing Library
- Flexibility: Users can build custom UIs while reusing orchestration
- Bundle Size: Consumers can tree-shake unused features
The vision is to refactor the codebase into three packages:
@virtual-json-diff/core # Headless orchestration logic
@virtual-json-diff/react # React adapter (current library)
@virtual-json-diff/vue # Vue adapter (future)
Core exports might include:
createDiffEngine(options)→ stateful diff enginepreprocessObjects(old, new, options)→ normalized objectsgenerateSegments(diff)→ segment arraybuildView(segments, diff)→ view modelgetChangeBlocks(diff, paths)→ navigation/review hunksgenerateMergedJson(diff, blocks, reviewStates)→ selective merge resultcalculateStats(diff)→ statisticscreateSearchIndex(view)→ search state
React adapter would consume core:
import { createDiffEngine, generateSegments } from '@virtual-json-diff/core';
import { useMemo } from 'react';
function useDiffView(oldValue, newValue, options) {
const engine = useMemo(() => createDiffEngine(options), [options]);
const diff = useMemo(() => engine.diff(oldValue, newValue), [oldValue, newValue]);
const segments = useMemo(() => generateSegments(diff[0]), [diff]);
// ... render with react-window
}- This is a future direction, not a breaking change
- Current users of
virtual-react-json-diffwill continue to work - The React adapter would be the default entry point
- Migration path TBD (likely major version bump)
To avoid overcommitment and maintain focus, the following are explicitly out of scope for now:
- Multi-framework support in this repository: Vue/Svelte adapters would be separate packages
- CLI tool: Diffing JSON from the command line
- Patch-file / unified-diff export: Generating
.patchor applying external patches - 3-way merge: Comparing more than two objects (base / ours / theirs)
- Non-JSON data: XML, YAML, TOML diffing
- Server-side rendering: Full SSR/SSG support (may work but not tested)
- Diff editing: Inline modification of JSON text inside the viewer
- Selective review/merge: Accept/reject change blocks and receive
mergedJsonviaonReviewChange - Programmatic navigation: Ref API + Arrow/j/k (and a/r in review mode)
- Horizontal scrolling: Currently relies on word wrap
- Line-level diffing for arrays: Compact single-line diffs for small arrays
- Undo/redo for review decisions: History for accept/reject and expand/collapse
- Custom diff algorithms: Plugin system for alternative diff engines
- Accessibility improvements: Stronger screen reader / ARIA coverage beyond current keyboard shortcuts
If you're interested in contributing to the headless refactor or have ideas for the architecture, please open a GitHub issue to discuss before submitting a PR.
For general contributions, see CONTRIBUTING.md (if it exists) or open an issue.