Skip to main content
They live on a subpath so the core API never imports React. That matters for two real cases: a plain Node script that parses NDEF, and any consumer whose bundler would otherwise pull React into a module that has no components in it. Each hook exists because of a specific mistake that is easy to make by hand and invisible when made:

Unmounting mid-scan

Navigating away leaves the iOS sheet up and Android’s controller held. The next scan fails with systemBusy, for reasons nowhere on screen.

Re-subscribing every render

An inline options object restarts reader mode on every render, which reads as “NFC randomly stops working” long before it reads as a dependency-array bug.

Prompting with NFC off

Android users can switch NFC off while your screen is open. A button saying “hold your card near the phone” is then a support ticket.

useNfcAvailability

Availability of NFC on this device, kept current.
boolean
NFC is present and switched on. This is what a screen usually asks.
boolean
The hardware exists.
boolean
It is switched on. A real answer on iOS, not a hardcoded true.
NfcCapabilities | null
Per-device capabilities: techs, hce, observeMode, pollingFrames, vas, backgroundReading, tagLost.
boolean
True until the first answer arrives.
() => void
Re-reads availability now. Rarely needed — changes arrive on their own.
Treat loading as “do not decide yet”. Before the first answer arrives, supported and enabled read false, so rendering the unsupported state without checking loading flashes “this device has no NFC” on a device that has NFC.
This subscribes rather than reading once, because the Android user can change the answer while your screen is open.

useNfcScan

One scan, driven from a component.
The second argument is the same ScanOptions every entry point takes.
() => Promise<T | null>
Starts a scan. Never rejects — the outcome lands in state, data and error, which is what a button’s onPress wants. Resolves to null when the scan failed or was cancelled.
() => Promise<T>
The same scan, but it rejects, for imperative flows. It rejects with exactly what was thrown, so an error your own work threw comes back unchanged.
() => void
Cancels a scan in progress. Safe to call when nothing is running.
() => void
Returns to idle, clearing data and error.
'idle' | 'scanning' | 'success' | 'error'
boolean
T | null
NfcError | null
Normalised to an NfcError, because a state field needs one type. Use scanAsync when you want the original throw.
Three things this handles that hand-written versions usually do not:
Navigating away mid-scan would otherwise leave the iOS sheet up and Android’s NFC controller held, and the next scan fails with systemBusy for reasons that are nowhere on screen.
Rather than starting a second session. A double tap is not a request for two sessions — and the platform would refuse the second anyway.
So they never need memoising, and a stale closure cannot read last render’s state.
Cancelling settles as idle rather than error: the user asked for it, so there is nothing to report to them.

useNfcTagStream

Reads tags continuously while the component is mounted. This is the kiosk shape: a door reader, a top-up terminal, a check-in desk.
boolean
default:"true"
Whether the stream should be running. Set it from screen focus. A reader-mode stream left running on a screen nobody is looking at keeps Android’s NFC controller held, so the next screen that wants a tag cannot have one — and nothing on that screen explains why.
boolean
Whether the stream is currently running.
NfcError | null
The failure that ended the stream, or null. Cleared when it restarts.
This is really an Android feature. Reader mode stays up indefinitely there. iOS caps a session at 60 seconds with a system sheet on screen throughout, so the stream ends with sessionTimeout and error is set. Restarting is left to you, because on iOS restarting means putting the sheet back up — a product decision rather than a technical one.
The listener is read at call time, so it never needs memoising. The options are compared by value, so an inline object literal does not restart the stream.

Without the hooks

Nothing here is required. Every hook is a thin wrapper over nfc.withTag, nfc.onTag and nfc.getAvailability, and using those directly is a supported, ordinary thing to do — you just take on the cancellation and subscription bookkeeping yourself.

Next

Sessions

What the hooks are wrapping.

Quickstart

A complete screen, end to end.