question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Broken backwards compatibility of dynamic rendering a `View` using the `render` prop

See original GitHub issue

Describe the bug

  1. A View whose content was rendered using the render prop function used to be rendered correctly on react-pdf v1 even if one of the sub-Views returned as its children was a Text that uses the render prop to render its content as well.
  2. On react-pdf v1 the return of the render prop function of a View could be a React.Fragment with as many Views as wanted. Now an error (TypeError: element.type is not a function) is thrown if the return of the render function is not a single View. 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.

image

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:

image

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

Issue Analytics

  • State:open
  • Created 2 years ago
  • Comments:8 (1 by maintainers)

github_iconTop GitHub Comments

2reactions
hoppulacommented, Jun 29, 2021

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} />
1reaction
diegomuracommented, Apr 15, 2021

Thanks @gabrielvincent ! and sorry 😦 I’ll try to see this asap

Read more comments on GitHub >

github_iconTop Results From Across the Web

7 things about React 16 your team should know - Pusher Blog
1. React 16 is backwards compatible · 2. Asynchronous rendering · 3. Concurrency · 4. Error Boundaries · 5. Component Stack Traces ·...
Read more >
react-virtualized/CHANGELOG.md at master - GitHub
React components for efficiently rendering large lists and tabular data ... and scrollLeft props and Grid in a way that was not backwards...
Read more >
Principles - Apache Tapestry
Principle 1 – Static Structure, Dynamic Behavior​​ Under no circumstance during the rendering or event processing of the page will you be able...
Read more >
Slow rendering - Android Developers
If your app suffers from slow UI rendering, then the system is forced to skip frames and the user will perceive stuttering in...
Read more >
What's New - deck.gl
Interleaving deck.gl layers with Google Maps vector rendering ... in multi-view applications, as long as each TileLayer instance is rendered into one view....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found