diff --git a/package.json b/package.json
index 0a3d26d..e473128 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@switchify/scanning",
- "version": "0.2.0",
+ "version": "0.3.0",
"description": "Switch scanning for React and React Native, ported from Switchify PC.",
"license": "AGPL-3.0-only",
"type": "module",
diff --git a/src/react/index.tsx b/src/react/index.tsx
index 5dbd9ec..3bd059c 100644
--- a/src/react/index.tsx
+++ b/src/react/index.tsx
@@ -90,10 +90,17 @@ export function ScanProvider({
return {children};
}
+let inert: ScanController | undefined;
+
+/**
+ * Outside a ScanProvider the hooks use one controller that is never started, so a
+ * shared component can call them whether or not its screen is scanned.
+ */
export function useScanController(): ScanController {
const controller = useContext(ControllerContext);
- if (!controller) throw new Error("Scanning hooks must be used inside a ScanProvider.");
- return controller;
+ if (controller) return controller;
+ inert ??= new ScanController();
+ return inert;
}
export function useScanSnapshot(): ScanSnapshot {
diff --git a/test/dom.test.tsx b/test/dom.test.tsx
index f08963d..d7c58fa 100644
--- a/test/dom.test.tsx
+++ b/test/dom.test.tsx
@@ -129,3 +129,9 @@ test("the latest handler runs without re-registering", async () => {
act(() => scanner.dispatch("select"));
expect(screen.getByTestId("count").textContent).toBe("2");
});
+
+test("hooks outside a provider render without scanning", async () => {
+ render( );
+ await flush();
+ expect(highlighted("alone")).toBe(false);
+});