Scientyfic World

Firebase ML Kit in React Native Is Going Away: How to Migrate Text Recognition

If you followed the 2023 version of this tutorial, which used @react-native-firebase/ml and the Cloud Vision API through Firebase ML to read text from images, it is time to migrate....

Share:

Get an AI summary of this article

Firebase ML kit with react native

If you followed the 2023 version of this tutorial, which used @react-native-firebase/ml and the Cloud Vision API through Firebase ML to read text from images, it is time to migrate. Firebase ML is deprecated, and the React Native Firebase package that wrapped it is going away. This post explains what changed and shows the two replacements: on-device text recognition with Google’s standalone ML Kit, and cloud extraction with Gemini through your own backend.

What Changed

  • Firebase ML is being shut down. The Firebase ML page now says it is deprecated and will be shut down on June 15, 2027. Projects that never used it cannot start, and existing users are told to migrate: Cloud Vision callers to Firebase AI Logic or the Cloud Vision API directly, and custom TensorFlow Lite models to Cloud Storage.
  • The React Native wrapper is discontinued. The npm deprecation notice on @react-native-firebase/ml (version 26.4) says the package “is discontinued and removed in React Native Firebase v27”, and points to Google ML Kit or Cloud Vision via Cloud Functions.
  • On-device ML Kit has been separate since 2020. Google moved ML Kit’s on-device APIs (text recognition, barcode scanning, face detection and others) into a standalone SDK in June 2020. It does not need a Firebase project at all.

Which Replacement Should You Use?

On-device ML KitGemini via your backend
RunsOn the phoneOn Google’s servers, called by your backend
Works offlineYesNo
CostNo per-request chargePay per request (Gemini API pricing)
PrivacyImages never leave the deviceImages are sent to your backend and Google
Best forFast, simple text capture: receipts, labels, documentsMessy images, handwriting, understanding or structuring the content
SetupNative module (needs a development build)A small server holding the API key

Many apps use both: on-device recognition for instant results and a cloud call only when the user asks for something smarter.

What I Tested

I wrote the code below in an Expo SDK 58 project (React Native 0.88) with @infinitered/react-native-mlkit-text-recognition 5.0.1 and expo-image-picker 58.0. It type-checks, and three Jest tests with the native modules mocked cover recognition, cancellation, the camera permission check and the receipt-total parser. The Gemini endpoint type-checks and passes a test with a fake Gemini client that checks the image is sent as inline data. I did not run recognition on a real device for this post, and I did not call the live Gemini API.

Option 1: On-Device Text Recognition With ML Kit

Install

npx expo install expo-image-picker
npm install @infinitered/react-native-mlkit-text-recognition

# ML Kit is native code, so use a development build, not Expo Go:
npx expo run:android   # or: npx expo run:ios

The library is an Expo module wrapping ML Kit text recognition for Android and iOS. Bare React Native apps can use it once Expo modules are installed (npx install-expo-modules); @react-native-ml-kit/text-recognition is another option for bare apps.

Pick an image and recognize text

recognizeText takes an image URI and returns the full text plus blocks, lines and elements with their positions:

import * as ImagePicker from 'expo-image-picker';
import { recognizeText } from '@infinitered/react-native-mlkit-text-recognition';

export type OcrResult = { text: string; lines: string[] };

// Let the user pick or take a photo, then run ML Kit text recognition on the device.
export async function pickAndRecognize(source: 'camera' | 'library'): Promise<OcrResult | null> {
  const options: ImagePicker.ImagePickerOptions = { mediaTypes: ['images'], quality: 1 };

  if (source === 'camera') {
    const { granted } = await ImagePicker.requestCameraPermissionsAsync();
    if (!granted) throw new Error('Camera permission is required to scan text.');
  }

  const result =
    source === 'camera'
      ? await ImagePicker.launchCameraAsync(options)
      : await ImagePicker.launchImageLibraryAsync(options);
  if (result.canceled) return null;

  const recognized = await recognizeText(result.assets[0].uri);
  return {
    text: recognized.text,
    lines: recognized.blocks.flatMap(block => block.lines.map(line => line.text)),
  };
}

// Example post-processing: find the largest amount on a receipt.
export function findTotal(lines: string[]): number | null {
  const amounts = lines
    .flatMap(line => line.match(/\d{1,3}(?:[,\s]\d{3})*(?:\.\d{2})/g) ?? [])
    .map(a => Number(a.replace(/[,\s]/g, '')));
  return amounts.length ? Math.max(...amounts) : null;
}

findTotal shows the kind of post-processing you usually need: ML Kit gives you text, and your app decides what it means. Real receipts vary a lot, so treat this as a starting point.

A screen to try it

import { useState } from 'react';
import { ActivityIndicator, Button, ScrollView, StyleSheet, Text, View } from 'react-native';
import { findTotal, pickAndRecognize, type OcrResult } from './recognize';

export function ScanScreen() {
  const [result, setResult] = useState<OcrResult | null>(null);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const scan = async (source: 'camera' | 'library') => {
    setBusy(true);
    setError(null);
    try {
      const r = await pickAndRecognize(source);
      if (r) setResult(r);
    } catch (e) {
      setError(e instanceof Error ? e.message : 'Could not read text from this image.');
    } finally {
      setBusy(false);
    }
  };

  const total = result ? findTotal(result.lines) : null;

  return (
    <View style={styles.container}>
      <View style={styles.row}>
        <Button title="Take photo" onPress={() => scan('camera')} disabled={busy} />
        <Button title="Choose image" onPress={() => scan('library')} disabled={busy} />
      </View>
      {busy && <ActivityIndicator />}
      {error && <Text style={styles.error}>{error}</Text>}
      {total !== null && <Text style={styles.total}>Total: {total.toFixed(2)}</Text>}
      <ScrollView>
        <Text selectable>{result?.text ?? 'Scan a receipt or document to see its text.'}</Text>
      </ScrollView>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16, paddingTop: 64, gap: 12 },
  row: { flexDirection: 'row', gap: 12 },
  error: { color: '#b91c1c' },
  total: { fontSize: 18, fontWeight: '600' },
});

On iOS, add camera and photo library usage descriptions; the expo-image-picker config plugin can set them in app.json. ML Kit’s text recognition supports Latin, Chinese, Devanagari, Japanese and Korean scripts.

Tests

import * as ImagePicker from 'expo-image-picker';
import { recognizeText } from '@infinitered/react-native-mlkit-text-recognition';
import { findTotal, pickAndRecognize } from '../src/ocr/recognize';

jest.mock('expo-image-picker', () => ({
  requestCameraPermissionsAsync: jest.fn(),
  launchCameraAsync: jest.fn(),
  launchImageLibraryAsync: jest.fn(),
}));
jest.mock('@infinitered/react-native-mlkit-text-recognition', () => ({ recognizeText: jest.fn() }));

const picker = ImagePicker as jest.Mocked<typeof ImagePicker>;
const ocr = recognizeText as jest.Mock;

test('runs recognition on the picked image and flattens lines', async () => {
  picker.launchImageLibraryAsync.mockResolvedValue({ canceled: false, assets: [{ uri: 'file:///receipt.jpg' }] } as any);
  ocr.mockResolvedValue({
    text: 'Coffee 3.50\nTotal 12.40',
    blocks: [{ lines: [{ text: 'Coffee 3.50' }, { text: 'Total 12.40' }] }],
  });
  const result = await pickAndRecognize('library');
  expect(ocr).toHaveBeenCalledWith('file:///receipt.jpg');
  expect(result?.lines).toEqual(['Coffee 3.50', 'Total 12.40']);
});

test('returns null when the user cancels, and refuses without camera permission', async () => {
  picker.launchImageLibraryAsync.mockResolvedValue({ canceled: true, assets: null } as any);
  expect(await pickAndRecognize('library')).toBeNull();
  picker.requestCameraPermissionsAsync.mockResolvedValue({ granted: false } as any);
  await expect(pickAndRecognize('camera')).rejects.toThrow('Camera permission');
});

test('findTotal picks the largest amount', () => {
  expect(findTotal(['Coffee 3.50', 'Bagel 4.25', 'TOTAL 1,234.56'])).toBe(1234.56);
  expect(findTotal(['no prices here'])).toBeNull();
});
$ npx jest __tests__/ocr.test.ts
✓ runs recognition on the picked image and flattens lines
✓ returns null when the user cancels, and refuses without camera permission
✓ findTotal picks the largest amount

Option 2: Extract Text With Gemini Through Your Backend

For handwriting, photos taken at odd angles or when you want structured output, a multimodal model does better than plain OCR. Keep the API key on a server; never ship it in the app. This endpoint accepts a base64 image and asks Gemini to transcribe it:

import express from 'express';
import type { GoogleGenAI } from '@google/genai';

const MODEL = process.env.GEMINI_MODEL ?? 'gemini-3.8-flash';
const ALLOWED = new Set(['image/jpeg', 'image/png', 'image/webp']);

// POST /extract { image: <base64>, mimeType: "image/jpeg" } -> { text }
export function createExtractRouter(ai: Pick<GoogleGenAI, 'models'>) {
  const router = express.Router();
  router.use(express.json({ limit: '8mb' }));

  router.post('/extract', async (req, res) => {
    const { image, mimeType } = req.body ?? {};
    if (typeof image !== 'string' || !image || !ALLOWED.has(mimeType)) {
      res.status(400).json({ error: 'Send a base64 JPEG, PNG or WebP image.' });
      return;
    }
    try {
      const response = await ai.models.generateContent({
        model: MODEL,
        contents: [
          {
            role: 'user',
            parts: [
              { inlineData: { mimeType, data: image } },
              { text: 'Transcribe all text in this image exactly, preserving line breaks. Return only the text.' },
            ],
          },
        ],
      });
      res.json({ text: response.text ?? '' });
    } catch (err) {
      console.error('Gemini request failed', err);
      res.status(502).json({ error: 'Text extraction is unavailable. Please try again.' });
    }
  });

  return router;
}

It plugs into the Express proxy from the React Native Gemini chatbot tutorial, which covers server setup, the API key and model choice. The app side sends the picked image as base64:

import * as ImagePicker from 'expo-image-picker';

const API_URL = 'https://api.example.com'; // your backend; the Gemini key stays there

// Cloud alternative: send the image to your backend, which asks Gemini to transcribe it.
export async function pickAndExtractInCloud(): Promise<string | null> {
  const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], base64: true, quality: 0.7 });
  if (result.canceled || !result.assets[0].base64) return null;
  const asset = result.assets[0];

  const res = await fetch(`${API_URL}/extract`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ image: asset.base64, mimeType: asset.mimeType ?? 'image/jpeg' }),
  });
  const data = await res.json();
  if (!res.ok) throw new Error(data.error);
  return data.text;
}

Firebase’s own recommendation for former Cloud Vision users is Firebase AI Logic, which lets apps call Gemini models with Firebase handling access control. Calling the Cloud Vision API directly from a backend is the other documented path if you want classic OCR in the cloud.

Test

import { test } from 'node:test';
import assert from 'node:assert/strict';
import express from 'express';
import type { AddressInfo } from 'node:net';
import { createExtractRouter } from '../src/extract.js';

test('sends the image as inline data and returns the text', async () => {
  let received: any;
  const fakeAi = { models: { generateContent: async (args: any) => { received = args; return { text: 'TOTAL 12.40' }; } } } as any;
  const server = express().use(createExtractRouter(fakeAi)).listen(0);
  const url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/extract`;

  const ok = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image: 'aGVsbG8=', mimeType: 'image/jpeg' }) });
  assert.equal(ok.status, 200);
  assert.deepEqual(await ok.json(), { text: 'TOTAL 12.40' });
  assert.deepEqual(received.contents[0].parts[0], { inlineData: { mimeType: 'image/jpeg', data: 'aGVsbG8=' } });

  const bad = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image: 'x', mimeType: 'application/pdf' }) });
  assert.equal(bad.status, 400);
  server.close();
});

Migration Checklist

  1. Remove @react-native-firebase/ml and any ml().cloudDocumentTextRecognizerProcessImage or similar calls.
  2. For on-device text, barcode or face features, install a standalone ML Kit library and create a development build.
  3. For cloud features, move calls to a backend (Gemini or Cloud Vision) and keep keys there.
  4. If you only enabled the Cloud Vision API for Firebase ML, review whether you still need it and the Blaze plan.
  5. Finish before Firebase ML shuts down on June 15, 2027.

Conclusion

Firebase ML is ending, but the features are still available: ML Kit runs text recognition free and offline on the device, and Gemini handles harder images from your backend. Pick on-device for speed, privacy and cost, cloud for accuracy on difficult inputs, and test the logic around them with mocks before trying it on real devices. For signing users in to an Expo app before they upload anything, see Firebase Auth with Expo.

FAQs

Is Firebase ML Kit deprecated?

Firebase ML is deprecated and will be shut down on June 15, 2027. ML Kit’s on-device APIs live on as a standalone Google SDK that does not need Firebase.

What replaces @react-native-firebase/ml?

The package is discontinued and removed in React Native Firebase v27. Use a standalone ML Kit library for on-device features, or call Cloud Vision or Gemini from a backend for cloud features.

Is on-device text recognition free?

ML Kit’s on-device APIs run on the phone without per-request charges, and they work offline.

Can I use ML Kit in Expo Go?

No. ML Kit is native code, so you need an Expo development build (npx expo run:android or run:ios) or a bare React Native app.

When should I use Gemini instead of ML Kit for text?

When images are messy or handwritten, or when you want the model to understand and structure the content, for example turning a receipt into line items. Use ML Kit when speed, offline support, privacy or cost matter most.

Should I call the Gemini API directly from the app?

Not with your API key. Keep the key on a backend, or use Firebase AI Logic, which is designed for calling Gemini from client apps with Firebase-managed access.

Snehasish Konger
Developed @scientyficworld.org | Technical writer @Nected | Content Developer
Connect with Snehasish Konger

On This page

Take a Pause with Intervals

A Sunday letter on building, writing, and thinking deeper as a developer — short, honest, and worth your time.

Snehasish Konger profile photo

"Hey there — I'm Snehasish. Hope this post saved you some head-scratching time! I've spent years turning technical chaos into clarity, and I'm here to be your guide through the maze of modern tech. Stick around for more lightbulb moments — we're just getting started."

Related Posts