diegomura / diegomura/react-pdf
Custom Page size with auto height triggers infinite loop in @react-pdf/renderer
- Dominant language
- TypeScript
- Stars
- 16.8k
- Forks
- 1.3k
- Avg merge
- 5h 6m
- Merged PRs (30d)
- 52
Description
## Environment
- **Package**: `@react-pdf/renderer`
- **Version**: `4.3.2`
- **React Version**: `19.2.1`
- **Node Version**: `22.14.0`
## Bug Description
Using custom numeric page sizes causes memory leaks and browser crashes in v4.3.2, even with the fixes from PR #3190 merged.
## Root Cause Analysis
### The Issue
In `packages/layout/src/page/isHeightAuto.ts`, the function checks `page.box?.height`:
```typescript
// File: packages/layout/src/page/isHeightAuto.ts
import isNil from '../utils/isNil';
const isHeightAuto = (page: SafePageNode) => isNil(page.box?.height);
export default isHeightAuto;
```
This function is called during pagination in `packages/layout/src/page/resolvePagination.ts`:
```typescript
// File: packages/layout/src/page/resolvePagination.ts
import isHeightAuto from './isHeightAuto';
const resolvePagination = (doc: SafeDocumentNode) => {
const pages = doc.children || [];
for (const page of pages) {
if (isHeightAuto(page)) { // ❌ Bug triggers here!
// Pagination logic that can loop infinitely
splitPage(page);
}
}
return doc;
};
```
### The Problem
When using custom page sizes without explicit height or with `height: 'auto'` (e.g., `size={{ width: 170.3 }}`):
1. **Step 1**: Only width is set in `page.style` by `resolvePageSizes`
2. **Step 2**: `page.box.height` should be populated by `resolveDimensions` after Yoga layout calculations
3. **Step 3**: `resolvePagination` calls `isHeightAuto()` **before** `page.box.height` is set
4. **Result**: Since `page.box.height` is undefined, `isHeightAuto()` returns `true`, triggering infinite pagination loops
### Layout Pipeline Order
From `packages/layout/src/index.ts`:
```typescript
const layout = asyncCompose(
resolveZIndex,
resolveOrigins,
resolveAssets,
resolvePagination, // ❌ Calls isHeightAuto() here
resolveTextLayout,
resolvePercentRadius,
resolveDimensions, // ✅ Sets page.box.height here (too late!)
resolveSvg,
resolveAssets,
resolveInheritance,
resolvePercentHeight,
resolvePagePaddings,
resolveStyles,
resolveLinkSubstitution,
resolveBookmarks,
resolvePageSizes, // ✅ Sets page.style.height here (first)
resolveYoga,
);
```
**The race condition**: `resolvePagination` runs before `resolveDimensions`, so `page.box.height` is still `undefined` when checked!
## Reproduction
### Minimal Example (Triggers Bug)
```tsx
import { PDFViewer, Document, Page, Text } from '@react-pdf/renderer';
function QRCodePreview() {
return (
{/* ❌ No height - triggers bug */}
Hello World
);
}
```
### Alternative Bug Triggers
```tsx
// Using style height: 'auto'
Hello World
// Custom height with many children (pagination needed)
{/* If content exceeds 200pt, pagination logic runs and checks isHeightAuto() */}
{Array.from({ length: 50 }).map((_, i) => (
Line {i}
))}
```
### Result
- Browser hangs
- Memory usage spikes
- Eventually crashes with `RangeError` or browser becomes unresponsive
### Working Example (Standard Sizes)
```tsx
{/* ✅ Works fine */}
Hello World
```
## Expected Behavior
Custom page sizes should work identically to standard sizes like `'A4'`, `'Letter'`, etc.
## Proposed Fix
### Option 1: Check Both Locations
Modify `packages/layout/src/page/isHeightAuto.ts`:
```typescript
const isHeightAuto = (page: SafePageNode) =>
isNil(page.box?.height) && isNil(page.style?.height);
```
### Option 2: Prioritize Style Height
```typescript
const isHeightAuto = (page: SafePageNode) => {
const height = page.box?.height ?? page.style?.height;
return isNil(height);
};
```
### Option 3: Use setNodeHeight Logic
Apply the same logic from `resolveDimensions.ts` line 77-80:
```typescript
// File: packages/layout/src/node/resolveDimensions.ts (existing working code)
const setNodeHeight = (node: SafeNode) => {
const value = isPage(node) ? node.box?.height : node.style?.height; // ✅ Checks both!
return setHeight(value);
};
```
Apply this pattern to `isHeightAuto.ts`:
```typescript
// File: packages/layout/src/page/isHeightAuto.ts (proposed fix)
import isNil from '../utils/isNil';
const isHeightAuto = (page: SafePageNode) => {
const height = page.box?.height ?? page.style?.height; // Check both locations
return isNil(height);
};
export default isHeightAuto;
```
## Related Issues
- User from PR #3190 reported same issue: https://github.com/diegomura/react-pdf/pull/3190#issuecomment-2375259397
- PR #3190: "Fix yoga error Invalid array length at Array.push()" - Fixed general infinite loops but not this specific case
- PR #2822: "Prevent infinite loop while splitting pages" - Earlier fix for different infinite loop scenario
## Additional Context
This bug only affects custom numeric page sizes. Standard page sizes work because they follow a different code path that doesn't encounter this timing issue.
**Impact**: Affects any application using QR codes, labels, or custom-sized documents that don't match standard page sizes.
Contributor guide
Assessment
This issue has not been assessed yet.