Common Patterns

Practical examples for the most common scanner use cases.

Scan once, show result, scan again

A very common pattern: scan a code, show the result to the user, wait for them to confirm, then let them scan another.

import React, { useRef, useState } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
import { Scanner } from 'react-native-scanner-pro';

export default function ScanOnceScreen() {
  const scannerRef = useRef(null);
  const [result, setResult] = useState(null);

  function handleScan(scanned) {
    setResult(scanned.data);
    // Camera is paused (because enableFreezeFrame is on)
  }

  function handleNext() {
    setResult(null);
    scannerRef.current?.resumeScanning();
  }

  return (
    <View style={{ flex: 1 }}>
      <Scanner
        ref={scannerRef}
        style={StyleSheet.absoluteFill}
        enableFreezeFrame={true}
        onCodeScanned={handleScan}
      />

      {result && (
        <View style={styles.card}>
          <Text style={styles.label}>Scanned:</Text>
          <Text style={styles.value}>{result}</Text>
          <Pressable style={styles.button} onPress={handleNext}>
            <Text style={styles.buttonText}>Scan another</Text>
          </Pressable>
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  card: {
    position: 'absolute',
    bottom: 40,
    left: 20,
    right: 20,
    backgroundColor: '#fff',
    borderRadius: 16,
    padding: 20,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.15,
    shadowRadius: 12,
    elevation: 8,
  },
  label: { fontSize: 12, color: '#999', marginBottom: 4 },
  value: { fontSize: 16, fontWeight: '600', marginBottom: 16 },
  button: {
    backgroundColor: '#007AFF',
    borderRadius: 10,
    paddingVertical: 12,
    alignItems: 'center',
  },
  buttonText: { color: '#fff', fontSize: 15, fontWeight: '600' },
});

Scanning with torch toggle

import React, { useState } from 'react';
import { View, Pressable, Text, StyleSheet } from 'react-native';
import { Scanner } from 'react-native-scanner-pro';

export default function TorchScreen() {
  const [torch, setTorch] = useState(false);

  return (
    <View style={{ flex: 1 }}>
      <Scanner
        style={StyleSheet.absoluteFill}
        torch={torch}
        onCodeScanned={(result) => console.log(result.data)}
      />

      <Pressable
        style={[styles.torchBtn, torch && styles.torchBtnActive]}
        onPress={() => setTorch(v => !v)}
      >
        <Text style={styles.torchIcon}>{torch ? '🔦' : '💡'}</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  torchBtn: {
    position: 'absolute',
    top: 60,
    right: 20,
    width: 48,
    height: 48,
    borderRadius: 24,
    backgroundColor: 'rgba(0,0,0,0.5)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  torchBtnActive: {
    backgroundColor: 'rgba(255,200,0,0.8)',
  },
  torchIcon: { fontSize: 22 },
});

Scan history list

Keep track of everything scanned in the current session:

import React, { useState } from 'react';
import { View, Text, FlatList, StyleSheet } from 'react-native';
import { Scanner } from 'react-native-scanner-pro';

export default function HistoryScanner() {
  const [history, setHistory] = useState([]);

  function handleScan(result) {
    setHistory(prev => {
      // Avoid duplicates
      if (prev.some(item => item.data === result.data)) return prev;
      return [{ data: result.data, type: result.type, id: Date.now() }, ...prev];
    });
  }

  return (
    <View style={{ flex: 1 }}>
      <Scanner
        style={styles.camera}
        onCodeScanned={handleScan}
        scanRegion={{ enabled: true, width: 280, height: 200, offsetY: -60 }}
      />

      <View style={styles.historyPanel}>
        <Text style={styles.historyTitle}>
          {history.length === 0 ? 'No scans yet' : `${history.length} code(s) scanned`}
        </Text>
        <FlatList
          data={history}
          keyExtractor={(item) => String(item.id)}
          renderItem={({ item }) => (
            <View style={styles.historyItem}>
              <Text style={styles.historyType}>{item.type}</Text>
              <Text style={styles.historyData} numberOfLines={1}>{item.data}</Text>
            </View>
          )}
        />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  camera: { flex: 1 },
  historyPanel: { height: 240, backgroundColor: '#fff', padding: 16 },
  historyTitle: { fontSize: 13, color: '#999', marginBottom: 8 },
  historyItem: {
    paddingVertical: 10,
    borderBottomWidth: StyleSheet.hairlineWidth,
    borderBottomColor: '#eee',
  },
  historyType: { fontSize: 11, color: '#999', marginBottom: 2 },
  historyData: { fontSize: 14, color: '#333' },
});

Professional checkout scanner

This is a more complete example that combines everything — scan region, freeze frame, pro overlay, haptic feedback, and a result card:

import React, { useRef, useState } from 'react';
import {
  View, Text, Pressable, StyleSheet, SafeAreaView
} from 'react-native';
import { Scanner } from 'react-native-scanner-pro';

export default function CheckoutScanner() {
  const scannerRef = useRef(null);
  const [scannedItem, setScannedItem] = useState(null);

  function handleScan(result) {
    setScannedItem(result);
    // Camera is paused by freeze frame
  }

  function handleDone() {
    setScannedItem(null);
    scannerRef.current?.resumeScanning();
  }

  return (
    <SafeAreaView style={{ flex: 1, backgroundColor: '#000' }}>
      <Scanner
        ref={scannerRef}
        style={StyleSheet.absoluteFill}
        enableFreezeFrame={true}
        scanRegion={{
          enabled: true,
          width: 300,
          height: 220,
          offsetY: -40,
          borderColor: '#FFFFFF',
          cornerRadius: 16,
          showHint: true,
          hintText: 'Scan a product barcode',
        }}
        onCodeScanned={handleScan}
      />

      {scannedItem && (
        <View style={styles.result}>
          <Text style={styles.resultType}>{scannedItem.type}</Text>
          <Text style={styles.resultData}>{scannedItem.data}</Text>
          <Pressable style={styles.doneBtn} onPress={handleDone}>
            <Text style={styles.doneBtnText}>Done</Text>
          </Pressable>
        </View>
      )}
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  result: {
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    backgroundColor: '#fff',
    borderTopLeftRadius: 24,
    borderTopRightRadius: 24,
    padding: 24,
    paddingBottom: 40,
  },
  resultType: {
    fontSize: 12,
    color: '#888',
    textTransform: 'uppercase',
    letterSpacing: 1,
    marginBottom: 6,
  },
  resultData: { fontSize: 22, fontWeight: '700', color: '#111', marginBottom: 20 },
  doneBtn: {
    backgroundColor: '#111',
    borderRadius: 12,
    paddingVertical: 14,
    alignItems: 'center',
  },
  doneBtnText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});

Preventing duplicate scans

If you're scanning continuously without freeze frame, the same code might fire multiple times. A simple way to prevent this is to track the last scanned value and use a cooldown:

const lastScanned = useRef(null);
const cooldown = useRef(false);

function handleScan(result) {
  if (cooldown.current) return;
  if (result.data === lastScanned.current) return;

  lastScanned.current = result.data;
  cooldown.current = true;

  console.log('New scan:', result.data);

  setTimeout(() => {
    cooldown.current = false;
  }, 1500); // 1.5 second cooldown
}

This works well for checkout-type flows where you want to accept the same code again after a short wait.