This guide maps every prop, callback and method of
react-native-pdf 7.x to
react-native-better-pdf, lists behaviour that changes silently, and shows
workarounds for what is not supported yet. Defaults and signatures below were
read from react-native-pdf 7.0.5's source, not from its README.
How this library handles what react-native-pdf users report most:
| Reported in react-native-pdf | react-native-better-pdf |
|---|---|
| Android 16 KB page size (#960 and others) | Native library 16 KB aligned |
| New Architecture / Fabric (#878 and others) | Built for Fabric on Nitro Views |
react-native-blob-util crashes and setup (#796 and others) |
Not used: downloads are native |
| Android "Already closed" crashes (#830 and others) | Closing a document while it renders, searches or exports is handled and stress-tested |
| Link taps on Android (#983 and others) | Every link form on every Android version (password-protected documents: see Limitations) |
| Search (#555 and others) | Built in, with highlights (Android: see the capability matrix) |
| Viewport events, restoring the position (#492 and others) | onViewportChange, initialViewport, goToViewport |
| Filling in and saving forms (#748 and others) | Fill, read and export (Android: see the capability matrix) |
| Cache control and offline reading (#952 and others) | Cache options on the source, PdfCache |
The full list, with reaction counts, is in community-requests.md.
- New Architecture only.
react-native-better-pdfis a Nitro View and needs Fabric and React Native 0.83+ (Expo SDK 55+). If your app still runs the legacy architecture, migrate that first. - Expo: works in development builds; Expo Go is not supported (same as react-native-pdf).
- Dependencies: remove
react-native-blob-utilif nothing else in your app uses it — downloads now happen natively. Addreact-native-nitro-modules.
npm uninstall react-native-pdf react-native-blob-util
npm install react-native-better-pdf react-native-nitro-modules
cd ios && pod install- import Pdf from 'react-native-pdf';
+ import { PdfView } from 'react-native-better-pdf';
- <Pdf
- source={{ uri: 'https://example.com/file.pdf', cache: true }}
+ <PdfView
+ source={{ uri: 'https://example.com/file.pdf' }}
- onLoadComplete={(numberOfPages) => setPageCount(numberOfPages)}
- onPageChanged={(page, numberOfPages) => setPage(page)}
- onError={(error) => console.warn(error)}
+ onLoad={({ pageCount }) => setPageCount(pageCount)}
+ onPageChange={({ page, pageCount }) => setPage(page + 1)}
+ onError={(error) => console.warn(error.code, error.message)}
style={{ flex: 1 }}
/>Read these first — none of them causes a type error, all of them change what users see.
| Topic | react-native-pdf | react-native-better-pdf | What to do |
|---|---|---|---|
| Page numbers | 1-based everywhere (page, setPage, onPageChanged, onPageSingleTap) |
0-based everywhere | Add or subtract 1 at the boundary. |
| TLS certificates | trustAllCerts defaults to true — invalid certificates are accepted |
Certificates are always validated | Fix the server's certificate. Downloads that "worked" before may now fail with network-error. |
| Download progress value | onLoadProgress(fraction) — 0…1, despite the name |
onLoadProgress({ loadedBytes, totalBytes? }) |
totalBytes === undefined ? undefined : loadedBytes / totalBytes |
| Loading UI | Shows xx.xx% text (or renderActivityIndicator) while downloading |
Shows nothing | Render your own indicator until onLoad, or onError without error.page. |
| Background | #EEE behind the pages |
Transparent | style={{ backgroundColor: '#EEE' }} |
| Fit | fitPolicy default 2 (fit whole page) |
'width' when vertical and continuous; 'page' with horizontal or enablePaging |
Pass fitPolicy="page" to keep react-native-pdf's default. |
| Max zoom | maxScale default 3 |
4 |
Pass maxScale={3} to keep the old limit. |
| Page gap | spacing default 10 |
pageSpacing default 8 |
Pass pageSpacing={10}. |
| Short documents | Centred vertically (#909, #920) | Start at the top in vertical continuous layouts, on both platforms | Nothing. |
| Rounded corners | borderRadius does not clip the pages on iOS (#920, #993) |
style.borderRadius clips the document on both platforms |
Nothing. |
| Safe area (iOS) | PDFKit adds the safe area as an inset, and going to a page ignores it: the first page can start under the status bar (#513, #994) | No automatic insets, like ScrollView and Android |
Keep the viewer below the status bar (e.g. in a SafeAreaView). |
| Current page | Platform-specific (iOS: PDFKit's current page) | Page under the viewport centre; the last page once scrolled to the end — identical on both platforms | Usually nothing; UI showing "page X" becomes consistent. |
| Remote file cache | Opt-in (source.cache), persistent, keyed by SHA-1 of the URL computed in JS |
Always: for the app session by default, across launches with cache: 'persistent'; keyed by URL + headers, or cacheKey |
cache: true → cache: 'persistent'; see Caching. |
| Errors | Error objects; the message is the only information |
PdfError struct with a stable code |
Match on error.code; see Errors. |
| react-native-pdf | react-native-better-pdf | Notes |
|---|---|---|
source={{ uri }} |
source={{ uri }} |
http(s)://, file://, content:// (Android). |
source={require('./x.pdf')} |
source={require('./x.pdf')} |
Same; no Metro config needed. |
source.headers |
source.headers |
Same, but sent to the URL's origin only: a redirect to another origin drops them. See Redirects. |
source.cache: true |
source.cache: 'persistent' |
Kept across launches, never purged by the OS. See Caching. |
source.cacheFileName |
source.cacheKey |
Identifies the copy instead of the URL and headers. |
source.expiration (seconds) |
source.maxAgeMs (milliseconds) |
An older copy is revalidated with the server (304 keeps it) instead of deleted, and still shown offline. |
source.method, source.body |
— | Only GET (body is read by react-native-pdf but missing from its types). |
source={{ uri: 'data:application/pdf;base64,…' }} |
source={{ base64: '…' }} |
Without the data:…;base64, prefix (1.1+); see Base64 documents. |
source={{ uri: 'bundle-assets://…' }} |
require() |
Bundle the file as an asset instead. |
page |
initialPage (0-based) + ref.goToPage() |
page is applied on change in react-native-pdf; here the initial page is a prop and later navigation is imperative. |
scale |
initialScale + ref.zoomTo(scale) |
Relative to the fitted page; applied once on load, like initialPage. |
minScale, maxScale |
minScale, maxScale |
Relative to the fitted page; maxScale default differs (3 → 4). |
spacing |
pageSpacing |
Default 10 → 8. |
password |
password |
Android: Android 15+, or Android 12–14 with SDK extension 13 (updated system PDF module); other devices report unsupported-pdf. react-native-pdf's bundled PDFium supported every version. |
showsVerticalScrollIndicator, showsHorizontalScrollIndicator |
same | Same names and defaults. |
horizontal, enablePaging |
same | Same names and defaults. Paging swipes along the layout axis (vertical unless horizontal) on both platforms. |
singlePage |
singlePage |
Shows initialPage (react-native-pdf: page); touches are ignored as before. Default fit is the whole page. |
enableRTL |
enableRTL |
Horizontal layouts only, page numbers in document order on both platforms. react-native-pdf on Android also reversed vertical layouts and page numbers; iOS (PDFKit) supports it only with enablePaging, as before. |
fitPolicy={0 | 1 | 2} |
fitPolicy="width" | "height" | "page" |
Named values. iOS applies one scale to the whole document (see the API docs). |
| — | spreadMode="pairs" | "book" |
New: facing pages side by side in the vertical continuous layout. |
nightMode (patched in on Android, #996) |
colorScheme="dark" |
Both platforms; hues kept rather than plainly inverted. |
scrollEnabled |
scrollEnabled |
Same name and default. Disables drag scrolling (also paging swipes); the drag goes to parent views. |
enableDoubleTapZoom |
enableDoubleTapZoom |
Same name and default. When false, onPageTap also fires without waiting for a possible second tap. |
enableAntialiasing |
— | Always on. |
enableAnnotationRendering |
— | Always on where the platform can: iOS draws every annotation; Android draws only some kinds (see Limitations). react-native-pdf's bundled PDFium drew all kinds on Android. |
enableTextSelection |
enableTextSelection |
Same name and default (true). false now turns selection off (react-native-pdf only stopped the event), and Android selects too (Android 15+). |
usePDFKit |
— | iOS always uses PDFKit. |
trustAllCerts |
— | Intentionally not supported. |
renderActivityIndicator, progressContainerStyle |
— | Render your own; see onLoadProgress. |
style and other View props |
same | |
| — | ref.print() |
New: the system print dialog for the loaded document, protected documents included. |
| — | contentInset={{ top, bottom }} |
New: pages scroll under a translucent header or toolbar (PR #993 in react-native-pdf, unmerged). |
| — | <PdfPageOverlay page x y width height> children |
New: React views on pages that follow scrolling and zoom, placed natively (#350, #254, #449, #484). |
| — | enableFormFilling, onFormChange, ref.getFormFields(), ref.setFormFieldValues(), ref.exportDocument() |
New: AcroForm filling by touch and from code, and the filled-in PDF as a file. |
| react-native-pdf | react-native-better-pdf |
|---|---|
onLoadComplete(numberOfPages, path, { width, height }, tableContents) |
onLoad({ pageCount, firstPageWidth, firstPageHeight, filePath }) — filePath is path (absolute, no file://); table of contents via ref.getOutline(); every page's size (not only the first) via ref.getPageSizes() |
onPageChanged(page, numberOfPages) — 1-based |
onPageChange({ page, pageCount }) — 0-based |
onLoadProgress(fraction) |
onLoadProgress({ loadedBytes, totalBytes? }) — throttled to ≤ 10 per second |
onError(error: Error) |
onError({ code, message, page?, httpStatus?, nativeDescription? }) |
onPageSingleTap(page, x, y) — 1-based, view coordinates |
onPageTap({ page, x, y }) — 0-based, PDF points from the page's top-left, identical on both platforms |
onPressLink(url) |
onLinkPress({ url }) — links to other pages are followed natively and not reported |
onScaleChanged(scale) |
onScaleChange({ scale }) — once per settled change, not during the gesture; relative to the fitted page |
onTextSelectionChange({ nativeEvent: { type, text } }) — per handle move |
onTextSelectionChange({ text }) — once per settled selection; text is '' when cleared |
onPageSingleTap coordinates were view pixels on Android and view points on
iOS; if you converted them to page coordinates yourself, remove that code.
const pdf = useRef(null);
- pdf.current?.setPage(5); // 1-based
+ pdf.current?.goToPage(4); // 0-based, animated
+ pdf.current?.goToPage(4, { animated: false });
+ pdf.current?.zoomTo(2); // replaces the `scale` propType the ref with PdfViewRef:
import type { PdfViewRef } from 'react-native-better-pdf';
const pdf = useRef<PdfViewRef>(null);react-native-pdf passes Error objects whose messages differ by platform.
Replace message matching with the stable code:
- onError={(error) => {
- if (String(error).includes('Password')) askForPassword();
- }}
+ onError={(error) => {
+ if (error.code === 'password-required') askForPassword();
+ else if (error.code === 'invalid-password') showWrongPassword();
+ }}| Situation | error.code |
|---|---|
No file / unreadable file:// |
file-not-found |
| Unsupported URI | invalid-source |
| Connection failed, TLS error, timeout | network-error |
Server answered non-2xx (error.httpStatus) |
http-error |
| Not a PDF, corrupt | invalid-pdf |
| Encrypted, no password given | password-required |
| Wrong password | invalid-password |
| Encrypted PDF on Android without password support (< 15, except 12–14 with SDK extension 13) | unsupported-pdf |
A page could not be rendered (error.page) |
page-render-failed |
| Out of memory | out-of-memory |
- const [page, setPage] = useState(1);
- <Pdf onPageChanged={(p, n) => { setPage(p); setTotal(n); }} />
- <Text>{page} / {total}</Text>
+ const [page, setPage] = useState(0);
+ <PdfView onPageChange={(e) => { setPage(e.page); setTotal(e.pageCount); }} />
+ <Text>{page + 1} / {total}</Text>react-native-pdf could only report the page. Save the full position (page, point on the page and zoom) and pass it back when the document is opened again:
<PdfView
source={source}
initialViewport={saved}
onViewportChange={({ viewport }) => save(viewport)}
/>react-native-pdf applied page whenever it changed. Keep that behaviour with
an effect:
const pdf = useRef<PdfViewRef>(null);
useEffect(() => {
pdf.current?.goToPage(targetPage - 1);
}, [targetPage]);
<PdfView ref={pdf} initialPage={targetPage - 1} source={source} />;- <Pdf
- onLoadProgress={(fraction) => setProgress(fraction)}
- renderActivityIndicator={(progress) => <ProgressBar value={progress} />}
- />
+ const [progress, setProgress] = useState<number | undefined>();
+ const [loaded, setLoaded] = useState(false);
+ <View style={{ flex: 1 }}>
+ <PdfView
+ onLoadProgress={({ loadedBytes, totalBytes }) =>
+ setProgress(totalBytes === undefined ? undefined : loadedBytes / totalBytes)
+ }
+ onLoad={() => setLoaded(true)}
+ />
+ {!loaded && <ProgressBar value={progress} />}
+ </View>react-native-pdf did not open links either; the callback shape changes:
- onPressLink={(url) => Linking.openURL(url)}
+ onLinkPress={({ url }) => {
+ // Validate: a PDF is untrusted input.
+ if (url.startsWith('https://')) Linking.openURL(url);
+ }}- <TouchableOpacity onPress={() => open(page)}>
- <Pdf source={source} singlePage page={page} style={styles.thumb} />
- </TouchableOpacity>
+ <Pressable onPress={() => open(page)}>
+ <PdfView source={source} singlePage initialPage={page - 1} style={styles.thumb} />
+ </Pressable>A singlePage view ignores touches, so the Pressable gets the tap and a
surrounding list scrolls from on top of it. Only that page is rendered.
For an image file instead (lists of <Image>s, sharing, uploads), render
the page once: await ref.current?.renderPage(page, { width }) returns
{ uri, width, height }.
react-native-pdf passed the outline to onLoadComplete (Android:
{ title, pageIdx, children, mNativePtr }; iOS: its own shape). It is now
read on demand, the same shape on both platforms, zero-based:
- onLoadComplete={(pages, path, size, tableContents) => setToc(tableContents)}
+ onLoad={async () => setToc(await pdf.current?.getOutline())}type PdfOutlineItem = { title: string; page?: number; children: PdfOutlineItem[] };page is undefined for entries that only group others or link elsewhere
(react-native-pdf reported pageIdx -1 or omitted them).
- onTextSelectionChange={({ nativeEvent }) =>
- setSelected(nativeEvent.type === 'selectionChanged' ? nativeEvent.text : '')
- }
+ onTextSelectionChange={({ text }) => setSelected(text)}The event now arrives once the selection settles (after the long press or
handle drag), not for every handle movement, and on Android too (Android
15+). A tap on the page clears the selection and is still an onPageTap.
react-native-pdf had no search. Here it is built in:
<PdfView ref={pdf} source={source} onSearchChange={setSearch} />;
pdf.current?.search('invoice');
pdf.current?.searchNext();Matches are highlighted natively; see the Search guide.
- onPageSingleTap={() => setChromeVisible((v) => !v)}
+ onPageTap={() => setChromeVisible((v) => !v)}Taps on the gaps between pages are not reported.
react-native-pdf re-downloaded on every mount unless source.cache was set,
and then kept the file until expiration. react-native-better-pdf always
keeps the download for the app session; the source's options cover the rest:
<PdfView
source={{
uri,
- cache: true,
- cacheFileName: documentId,
- expiration: 24 * 60 * 60,
+ cache: 'persistent',
+ cacheKey: documentId,
+ maxAgeMs: 24 * 60 * 60 * 1000,
}}
+ onLoad={({ isStale }) => setOfflineBanner(isStale)}
/>- After
maxAgeMsthe copy is not deleted: the server is asked whether it changed (ETag/Last-Modified). When the server cannot be reached, the view shows the stored copy andonLoadreportsisStale: true. - Persistent copies are not purged by the OS and not backed up. Remove them
with
PdfCache.remove(source)/PdfCache.clear(). - Downloading ahead of time ("available offline"), checking for a copy and deleting it need no view and no file-system library:
import { PdfCache } from 'react-native-better-pdf';
await PdfCache.download(source, { onProgress, signal });
const entry = await PdfCache.getEntry(source); // undefined when not storedSee Offline reading and caching.
Pass the base64 itself as { base64 } (1.1+), without the data: prefix:
// react-native-pdf
<Pdf source={{ uri: `data:application/pdf;base64,${base64}` }} />
// react-native-better-pdf
<PdfView source={{ base64 }} />react-native-pdf wrote the data to a cache file itself (named, without
cacheFileName, by a SHA-1 of the whole string computed in JS, and written
with react-native-blob-util), and kept the file. Here the string is decoded
natively and onLoad's filePath is undefined. Nothing stays on disk
once the document closes: iOS keeps a temporary copy only while it is
shown, Android not even that.
Line breaks in the base64 are ignored; URL-safe base64 is not accepted.
While the document is shown, its text is held twice (your string and a
native copy). Write large documents to a file and open the file, and if the
base64 comes from your own API, consider returning
the PDF itself and passing its URL with headers: it is then downloaded
natively, with progress, caching and offline reading. See
Base64 documents.
| Feature | Status |
|---|---|
| Passwords on Android 7–11, and 12–14 without SDK extension 13 | Not possible with the platform renderer |
| Windows | Not planned |
| Links in password-protected documents on Android < 15 (except 12–14 with SDK extension 13), and their internal links other than GoTo actions with XYZ destinations | Platform limitation — see Limitations (links in other documents work on every version) |
| Text selection on Android < 15 (except 12–14 with SDK extension 13); selections across pages on Android | Platform limitation below 15; cross-page selection not yet — see Limitations |
If one of these blocks your migration, please open an issue describing the use case.