react-pdf
react-pdf copied to clipboard
Broken backwards compatibility of dynamic rendering a `View` using the `render` prop
Describe the bug
- A
View
whose content was rendered using therender
prop function used to be rendered correctly onreact-pdf
v1 even if one of the sub-View
s returned as its children was aText
that uses therender
prop to render its content as well. - On
react-pdf
v1 the return of therender
prop function of aView
could be aReact.Fragment
with as manyView
s as wanted. Now an error (TypeError: element.type is not a function
) is thrown if the return of therender
function is not a singleView
. This can be seen happening here on the REPL.
In this issue I'll use the Footer
component of my project for code examples. This is how this footer looks like. Note that the layout of the footer changes depending on the page number: it's aligned to the right on even pages and to the left on even pages. That's why I need to use the render
prop function of a View
and return in it a Text
that is also rendered using its own render
prop function. The pageNumber
in the View
is used to determine the layout of the footer.
The reason why I need to also render the content of Text
using the render
prop function is because only Text
can give me totalPages
, which I use as a flag to differentiate between the first render from the last one, when all the pages are already at their final position. I can then properly instruct my PageTracker
class about keeping a count of page numbers and at what page each chapter starts. This is crucial in order to enable a Table of Contents.
Here's the Footer
component's code which works on react-pdf
v1.6.14:
import React, { Fragment } from 'react'
import { Text, View } from '@react-pdf/renderer'
import { isEmpty, isNil } from 'lodash'
import { useReportContext } from 'core/hooks/useReportContext'
import { getFullName } from 'core/utils/BirthInfo'
import type ReactPDF from '@react-pdf/renderer'
import type { FC, ReactElement } from 'react'
import {
absolute,
bottomPt,
exoLight,
exoSemiBold,
f,
flex,
flexColumn,
flexRow,
flexRowReverse,
hPt,
itemsEnd,
itemsStart,
justifyBetween,
mt,
pb,
phPercent,
w100,
} from '../../style'
interface FooterProps {
title: string
chapterName?: string
forcePageNumber?: number
}
export const Footer: FC<FooterProps> = ({
title,
chapterName,
forcePageNumber,
}) => {
const { birthInfo, pageTracker, theme } = useReportContext()
const name = getFullName(birthInfo)
const textColor: ReactPDF.Style = {
color: theme.footer.textColor,
}
const dividerColor: ReactPDF.Style = {
backgroundColor: theme.footer.dividerColor,
}
return (
<View
fixed
style={[w100, phPercent(10), absolute, bottomPt(0), pb(5)]}
render={({ pageNumber }): ReactElement => {
pageNumber = forcePageNumber ?? pageNumber
const rowDirection = (pageNumber + pageTracker.totalPages).isEven()
? flexRow
: flexRowReverse
const alignment = (pageNumber + pageTracker.totalPages).isEven()
? itemsStart
: itemsEnd
return (
<Fragment>
<View style={[w100, hPt(3), dividerColor]} />
<View
style={[
flex,
rowDirection,
justifyBetween,
itemsStart,
mt(2),
textColor,
]}
>
<View style={[flex, flexColumn, alignment, f(6)]}>
<Text style={[exoSemiBold]}>{name}</Text>
<Text style={[exoLight]}>{title}</Text>
</View>
<Text
style={[f(7), exoSemiBold]}
render={({ pageNumber, totalPages }): Optional<string> => {
if (isNil(totalPages)) {
return
}
pageNumber = forcePageNumber ?? pageTracker.totalPages
pageTracker.registerPage()
pageTracker.registerChapterPage(chapterName)
if (!isEmpty(chapterName)) {
{/* Registers beginning of chapter. This enables the Table of Contents */}
pageTracker.registerChapterStartPage(
pageNumber,
chapterName
)
}
return pageNumber.asDoubleDigitsString()
}}
/>
</View>
</Fragment>
)
}}
/>
)
}
On react-pdf
v2.0.4 this code produces the TypeError: element.type is not a function
.
I can stop that error from happening if I replace the Fragment
with react-pdf
's View
primitive:
export const Footer: FC<FooterProps> = ({
title,
chapterName,
forcePageNumber,
}) => {
const { birthInfo, pageTracker, theme } = useReportContext()
const name = getFullName(birthInfo)
const textColor: ReactPDF.Style = {
color: theme.footer.textColor,
}
const dividerColor: ReactPDF.Style = {
backgroundColor: theme.footer.dividerColor,
}
return (
<View
fixed
style={[w100, phPercent(10), absolute, bottomPt(0), pb(5)]}
render={({ pageNumber }): ReactElement => {
pageNumber = forcePageNumber ?? pageNumber
const rowDirection = (pageNumber + pageTracker.totalPages).isEven()
? flexRow
: flexRowReverse
const alignment = (pageNumber + pageTracker.totalPages).isEven()
? itemsStart
: itemsEnd
return (
// <Fragment> Replaced this with the View below
<View>
<View style={[w100, hPt(3), dividerColor]} />
<View
style={[
flex,
rowDirection,
justifyBetween,
itemsStart,
mt(2),
textColor,
]}
>
<View style={[flex, flexColumn, alignment, f(6)]}>
<Text style={[exoSemiBold]}>{name}</Text>
<Text style={[exoLight]}>{title}</Text>
</View>
<Text
style={[f(7), exoSemiBold]}
render={({ pageNumber, totalPages }): Optional<string> => {
if (isNil(totalPages)) {
return
}
pageNumber = forcePageNumber ?? pageTracker.totalPages
pageTracker.registerPage()
pageTracker.registerChapterPage(chapterName)
if (!isEmpty(chapterName)) {
{
/* Registers beginning of chapter. This enables the Table of Contents */
}
pageTracker.registerChapterStartPage(
pageNumber,
chapterName
)
}
return pageNumber.asDoubleDigitsString()
}}
/>
</View>
</View>
// <Fragment> Replaced this with the View above
)
}}
/>
)
}
However, this is the result I get from the code above:
A much simpler example of this can be seen here on the REPL. After some tests, it became evident that even a simple dynamic render is not working. In this example on the REPL I could not render a simple View
as the return of the render
prop function. I expected to see a Text
with a green background inside a blue View
inside a red View
. Only the red, outermost View
is rendered.
I couldn't find a way to run the REPL using other versions of react-pd
, which could be useful to test these scenarios.
Environment:
- OS: macOS
- Node v14.15.0
- React-pdf version: v2.0.4
Thanks @gabrielvincent ! and sorry :( I'll try to see this asap
I'm experiencing a similar issue with dynamic rendering in the newest version (2.0.8). Rendering dynamic content of a Text
component causes the PDF to be re-rendered over and over again nonstop. This did not occur in v1.x
Same issue by my side. In order to stop the infinite re-rendering, needed to unmount the footer component which manage the dynamic page number. This happened upgrading to v 2.x.
Here is the component who is causing the issue:
import React from "react";
import {Text, StyleSheet } from "@react-pdf/renderer";
const styles = StyleSheet.create({
pageNumber: {
position: "absolute",
fontSize: 12,
bottom: 30,
left: 0,
right: 0,
textAlign: "center",
color: "grey",
fontFamily: "Nunito",
},
});
export default () => (
<Text
style={styles.pageNumber}
render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`}
fixed
/>
);
I'm also seeing this issue with dynamic rendering in a WebvView (via BlobProvider
). I get into an infinite loop with:
<Document>
<Page size="A4" orientation="landscape">
<Text
render={({ pageNumber, totalPages }) => {
console.log('render', pageNumber, totalPages);
return `${pageNumber} / ${totalPages}`;
}}
fixed
/>
</Page>
</Document>
I'll see if I can dig through the code a bit and find a cause
@diegomura any update on this issue this is breaking all our pdfs right now.
I noticed that you can wrap render
function with useCallback
as a workaround to prevent infinite re-renders, e.g.
const pageNumbers = useCallback(({ pageNumber, totalPages }) => {
return `${pageNumber} / ${totalPages}`;
}, []);
<Text render={pageNumbers} />
This is a brilliant solution. Do you mind explaining a little more about how you came to this solution and more elaborate more on how this actually worked?
Either I'm missing something or this was never fixed, but I cannot get any dynamic content to render using using the render prop or calling jsx expressions within Text or View components. I keep getting the same TypeError: element.type is not a function as mentioned in the OP. Was there any resolution on this?