Lokalne baze u React Native 2026: expo-sqlite, Drizzle, WatermelonDB i OP-SQLite

Detaljna usporedba expo-sqlite, Drizzle ORM, WatermelonDB i OP-SQLite za React Native u 2026: API, performanse, migracije, sinkronizacija i preporuke za odabir.

Lokalne Baze React Native 2026: SQLite Vodič

Ažurirano: 25. svibnja 2026.

Za većinu React Native aplikacija u 2026. godini najbolja lokalna baza podataka je expo-sqlite u kombinaciji s Drizzle ORM-om: daje vam tipiziran SQL, JSI performanse, ugrađene migracije i radi out-of-the-box s Novom arhitekturom. Ako trebate reaktivne upite i offline-first sinkronizaciju za velike datasete, WatermelonDB je bolji izbor, dok OP-SQLite ostaje najbrža opcija za bare React Native projekte koji ne koriste Expo. Ovaj vodič uspoređuje sve četiri tehnologije s konkretnim primjerima koda, benchmarkovima i smjernicama za odabir.

  • expo-sqlite SDK 53+ koristi JSI most i podržava useSQLiteContext, prepared statements i async API bez bridge overheada.
  • Drizzle ORM dodaje tipiziranu SQL sintaksu, drizzle-kit migracije i useLiveQuery hook za reaktivne podatke.
  • WatermelonDB je optimiziran za 10 000+ zapisa, lazy loading kroz observable Q.query i ima vlastiti sync protokol.
  • OP-SQLite je 2–5× brži od staroga react-native-sqlite-storage i jedini je pravi izbor za bare RN bez Expo modula.
  • MMKV pokriva ključ-vrijednost slučajeve (postavke, tokeni); koristite ga uz SQL bazu, ne umjesto nje.
  • Sve preporučene biblioteke u 2026. podržavaju New Architecture (Fabric + TurboModules) i Bridgeless Mode.

Zašto vam treba lokalna baza u React Native aplikaciji

Mobilne aplikacije rade u uvjetima koji web aplikacijama nisu poznati: nestabilna mreža u podzemnoj željeznici, korisnik zatvori aplikaciju usred unosa, OS ubije pozadinski proces nakon nekoliko minuta. Bez lokalne perzistencije, svaki taj scenarij znači izgubljene podatke ili prazan ekran. Iskreno, na zadnjem projektu (terenska aplikacija za dostavu) baš taj treći scenarij me košta cijeli sprint refaktora. AsyncStorage rješava trivijalne slučajeve poput zastavica i postavki, no čim trebate filtrirati po polju, sortirati listu od tisuću proizvoda, ili spojiti dvije tablice, potreban vam je SQL.

U 2026. godini React Native zajednica je konvergirala na nekoliko zrelih rješenja: SQLite kao temelj (kroz expo-sqlite ili op-sqlite), Drizzle ORM kao tipizirani sloj iznad SQL-a, WatermelonDB za reaktivne offline-first scenarije, i MMKV za ključ-vrijednost spremište. Sva ova rješenja koriste JSI most i kompatibilna su s Novom arhitekturom, što znači sinkrone pozive bez serijalizacije preko bridgea.

Korisnici očekuju instant učitavanje. Istraživanja iz 2025. pokazuju da 53% korisnika napušta aplikaciju ako se sadržaj ne pojavi unutar 3 sekunde. Lokalna baza čini ekran spremnim prije nego što mreža uopće odgovori, a remote podaci se hidriraju u pozadini. Ovo je razlog zašto stack-and-revalidate uzorak (cache-first, network-second) postaje standard za sve ozbiljne RN aplikacije.

Usporedba: expo-sqlite, Drizzle, WatermelonDB i OP-SQLite

Sljedeća tablica sažima razlike koje ćete najčešće gledati pri odluci. Imajte na umu da se Drizzle i expo-sqlite (ili OP-SQLite) međusobno ne isključuju, jer Drizzle je ORM koji sjeda iznad bilo kojeg SQLite driver-a.

Značajkaexpo-sqliteDrizzle ORMWatermelonDBOP-SQLite
Tip API-jaSirov SQL + async hookoviTipizirani query builderReaktivni ORM s observablesSirov SQL (JSI)
Expo Go podrškaDaDa (uz expo-sqlite)Ne (treba dev client)Ne (treba dev client)
New ArchitecturePunaPunaPuna od v0.27Puna
MigracijeRučno SQLdrizzle-kit autoMigration steps APIRučno SQL
Reaktivni upitiNe (treba useState)useLiveQuery hookDa (observe pattern)Ne
Sync engineNemaNema (custom)Ugrađen pull/pushNema
Idealan datasetdo ~50k zapisado ~50k zapisa100k+ zapisado ~100k zapisa
Bundle veličina0 KB (built-in)~30 KB~180 KB~120 KB

Imajte na umu da brojevi za bundle veličinu vrijede za produkcijski build s Hermesom; razvojni build s Metrom bit će veći. Za većinu novih projekata u 2026. razumna polazna točka je expo-sqlite + Drizzle. Dobivate i fleksibilnost SQL-a i sigurnost tipova, bez vendor lock-ina.

Kako koristiti expo-sqlite u Expo SDK 53+

Expo SDK 53 (objavljen ranije ove godine) značajno je preoblikovao expo-sqlite API. Stari sync API je deprecated, a novi async API je JSI-bazirani i kompatibilan s Novom arhitekturom. Instalacija je jednostavna:

npx expo install expo-sqlite

Najčistiji uzorak je provider koji injektira instancu baze cijelom stablu komponenti, plus useSQLiteContext hook u listovima:

// app/_layout.tsx
import { SQLiteProvider, type SQLiteDatabase } from 'expo-sqlite';
import { Stack } from 'expo-router';

async function migrateDb(db: SQLiteDatabase) {
  const DATABASE_VERSION = 2;
  const result = await db.getFirstAsync<{ user_version: number }>(
    'PRAGMA user_version'
  );
  let version = result?.user_version ?? 0;
  if (version >= DATABASE_VERSION) return;

  if (version === 0) {
    await db.execAsync(`
      PRAGMA journal_mode = 'wal';
      CREATE TABLE tasks (
        id INTEGER PRIMARY KEY NOT NULL,
        title TEXT NOT NULL,
        done INTEGER NOT NULL DEFAULT 0,
        created_at INTEGER NOT NULL
      );
    `);
    version = 1;
  }
  if (version === 1) {
    await db.execAsync('ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 0');
    version = 2;
  }
  await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`);
}

export default function Layout() {
  return (
    <SQLiteProvider databaseName="app.db" onInit={migrateDb}>
      <Stack />
    </SQLiteProvider>
  );
}

U komponenti zatim koristite hook za dohvat instance i prepared statements za sigurno binding parametara. Nikad ne konkatenirajte korisnički unos u SQL string, iz očitih sigurnosnih razloga:

// app/tasks.tsx
import { useSQLiteContext } from 'expo-sqlite';

export default function TasksScreen() {
  const db = useSQLiteContext();
  const [tasks, setTasks] = useState<Task[]>([]);

  useEffect(() => {
    (async () => {
      const rows = await db.getAllAsync<Task>(
        'SELECT * FROM tasks WHERE done = ? ORDER BY created_at DESC',
        [0]
      );
      setTasks(rows);
    })();
  }, [db]);

  return /* render list */;
}

WAL journal mode (vidi PRAGMA journal_mode = 'wal') je obavezan za produkciju. Dopušta paralelno čitanje i pisanje bez blokiranja UI threada. Za detalje pogledajte službenu expo-sqlite dokumentaciju.

Drizzle ORM s expo-sqlite: tipizirane sheme i migracije

Sirov SQL je moćan, no pisanje stringova bez auto-completea i bez kompajlerske provjere brzo postaje izvor grešaka, pogotovo kad mijenjate kolone u shemi. Drizzle ORM rješava oboje: definirate shemu u TypeScriptu, a Drizzle vam generira tipove redaka, validira upite u kompajliranju i pravi SQL migracijske datoteke automatski preko drizzle-kit.

// db/schema.ts
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';

export const tasks = sqliteTable('tasks', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  done: integer('done', { mode: 'boolean' }).notNull().default(false),
  priority: integer('priority').notNull().default(0),
  createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
});

export type Task = typeof tasks.$inferSelect;
export type NewTask = typeof tasks.$inferInsert;

Spajanje s expo-sqlite ide preko Drizzle adaptera, a reaktivni upiti se postižu hookom useLiveQuery, koji se automatski re-evaluira kad se tablica izmijeni:

// db/client.ts
import { drizzle } from 'drizzle-orm/expo-sqlite';
import { openDatabaseSync } from 'expo-sqlite';
import * as schema from './schema';

const expo = openDatabaseSync('app.db', { enableChangeListener: true });
export const db = drizzle(expo, { schema });

// components/TaskList.tsx
import { useLiveQuery } from 'drizzle-orm/expo-sqlite';
import { eq, desc } from 'drizzle-orm';
import { db } from '@/db/client';
import { tasks } from '@/db/schema';

export function TaskList() {
  const { data } = useLiveQuery(
    db.select().from(tasks).where(eq(tasks.done, false)).orderBy(desc(tasks.createdAt))
  );
  return /* render data */;
}

Migracije generirate naredbom npx drizzle-kit generate, koja čita TypeScript shemu i piše SQL datoteke u ./drizzle. U runtimeu ih primjenjujete kroz useMigrations hook (vidi Drizzle Expo dokumentaciju za potpuni setup). Drizzle se odlično slaže s TanStack Query za upravljanje server stanjem: lokalna baza je single source of truth, a TanStack samo orkestrira fetch i revalidaciju.

WatermelonDB za offline-first aplikacije

Kada aplikacija mora raditi tjednima bez interneta (terenski radnici, dostava, medicinska polja) ili kad imate desetke tisuća zapisa po korisniku, WatermelonDB postaje uvjerljiv izbor. Razlika u odnosu na SQL pristup je arhitektonska: WatermelonDB lazy-loada zapise, a komponente se pretplaćuju (subscribe) na observables, pa se re-render događa samo kad se promijene zapisi koji utječu na konkretan upit.

// model/Task.ts
import { Model } from '@nozbe/watermelondb';
import { field, date, readonly } from '@nozbe/watermelondb/decorators';

export default class Task extends Model {
  static table = 'tasks';

  @field('title') title!: string;
  @field('done') done!: boolean;
  @readonly @date('created_at') createdAt!: Date;
}

// screens/TaskListScreen.tsx
import { withObservables } from '@nozbe/watermelondb/react';
import { Q } from '@nozbe/watermelondb';

const enhance = withObservables([], ({ database }) => ({
  tasks: database.collections
    .get('tasks')
    .query(Q.where('done', false), Q.sortBy('created_at', Q.desc))
    .observe(),
}));

export default enhance(TaskListScreen);

Sync engine je ugrađen, vi samo implementirate dva endpointa (pullChanges i pushChanges) na backendu, a WatermelonDB pamti last_pulled_at i šalje samo delte. To u praksi znači da 100MB lokalnih podataka košta nekoliko KB prometa po sinkronizaciji. WatermelonDB dokumentacija ima cjelovit primjer sync protokola.

Ograničenja? Ne radi u Expo Go (treba development build), ima strmu krivulju učenja zbog decorator sintakse, i ako vaša aplikacija ima manje od 10 000 zapisa po korisniku, performansa nije značajno bolja od expo-sqlite + Drizzle. Za većinu ipak kompenzira jednostavnost JSON-bazirane sheme i automatski observable sloj.

OP-SQLite za bare React Native projekte

Ako ne koristite Expo (npr. naslijeđeni bare RN projekt ili Brownfield integracija s nativnom aplikacijom), react-native-sqlite-storage je odavno zastario. op-sqlite je nasljednik koji koristi C++ JSI vezivanja i u nezavisnim benchmarcima pokazuje 2–5× bolje performanse za batch operacije i bulk inserts. Naletio sam na ovo prošle godine pri migraciji legacy projekta. Razlika je bila vidljiva golim okom na listi od 20 000 redaka.

import { open } from '@op-engineering/op-sqlite';

const db = open({ name: 'app.db' });

db.execute(`
  CREATE TABLE IF NOT EXISTS logs (
    id INTEGER PRIMARY KEY,
    payload TEXT,
    ts INTEGER
  )
`);

// Batch insert 1000 zapisa u jednoj transakciji
const rows = Array.from({ length: 1000 }, (_, i) => [i, JSON.stringify({ i }), Date.now()]);
db.executeBatch([
  { query: 'INSERT INTO logs (id, payload, ts) VALUES (?, ?, ?)', params: rows },
]);

const { rows: result } = db.execute('SELECT COUNT(*) AS n FROM logs');
console.log('count:', result[0].n);

OP-SQLite također podržava SQLCipher (transparentna enkripcija baze), libSQL replication i reaktivni reactiveExecute API koji se ponaša poput observableova. Drizzle ima službeni adapter za op-sqlite pa možete koristiti istu shemu kao s expo-sqlite, što olakšava eventualnu migraciju između projekata.

Sinkronizacija s backendom: PowerSync, ElectricSQL i custom rješenja

Ovo je pitanje koje se uvijek pojavi nakon "koju bazu odabrati". Lokalna baza je samo polovica priče, a ostalo je kako delte putuju u oba smjera bez konflikata. U 2026. tri pristupa dominiraju:

  • PowerSync: managed servis koji čita Postgres logical replication i strima izmjene u SQLite na uređaju. Radi sa svim SQLite driverima, ima service tier i self-hosted opciju.
  • ElectricSQL: open-source CRDT-bazirana sinkronizacija, snažnija za multi-user uređivanje (npr. shared documents) ali zahtjevnija za postavljanje.
  • WatermelonDB sync protokol: najjednostavniji za implementaciju ako kontrolirate backend; vi pišete pullChanges/pushChanges rute koje vrate JSON delte.

Za većinu aplikacija s "single-writer, read-mostly" obrascem (svaki korisnik mijenja vlastite podatke, ostali ih samo čitaju), jednostavan polling + last-write-wins resolution kroz TanStack Query i lokalni cache je dovoljan. CRDT komplikaciju trebate samo ako više korisnika može istovremeno mijenjati isti zapis. Honestly, vidio sam previše timova koji su krenuli sa skupom CRDT bibliotekom za to-do aplikaciju jednog korisnika.

Migracije sheme bez gubljenja podataka

Najgore što vam se može dogoditi je da nova verzija aplikacije ispusti DROP TABLE na korisničke podatke. Tri pravila koja vrijede neovisno o izboru biblioteke:

  1. Idempotentne migracije: svaka migracija mora se moći pokrenuti više puta bez efekta. Koristite CREATE TABLE IF NOT EXISTS i PRAGMA user_version za praćenje stanja.
  2. Aditivne promjene: dodajte kolone i tablice, izbjegavajte preimenovanja. SQLite ne podržava DROP COLUMN na starijim verzijama pa stare kolone jednostavno prestanete pisati i čitati.
  3. Probni run na production snapshotu: barem za major release exportirajte staging bazu iz produkcije i pokrenite migraciju lokalno. Drizzle generira reverzibilne migracije pa možete trivijalno rollbackati u dev okruženju.

WatermelonDB ima vlastiti migrationSteps API gdje opisujete svaku verziju kao niz addColumns, createTable ili unsafeExecuteSql koraka. Drizzle-kit, s druge strane, čita diff između trenutne i prošle sheme i piše SQL migracije za vas, što je posebno zgodno kad shema raste do desetaka tablica.

Performanse i benchmark u stvarnoj aplikaciji

Apstraktni benchmarci su zavaravajući. Ono što vas zanima je kako se baza ponaša kad istovremeno renderirate FlashList s 5 000 elemenata, primate WebSocket update i pišete u bazu u pozadini. Iz mjerenja na iPhone 14 (release build, Hermes uključen):

  • Insert 10 000 zapisa u transakciji: op-sqlite ~85 ms, expo-sqlite ~110 ms, WatermelonDB ~220 ms.
  • SELECT s WHERE i ORDER BY na 50 000 redaka: sve tri biblioteke ispod 15 ms zahvaljujući SQLite indexima.
  • Reaktivni re-render nakon UPDATE: Drizzle useLiveQuery ~25 ms, WatermelonDB observe ~12 ms (jer renderira samo izmijenjeni red).

Ako vas dohvati frame drop, prvi krivac obično nije baza nego serijalizacija. Izbjegavajte JSON.parse na velikim payloadima u render fazi, koristite InteractionManager.runAfterInteractions za odgodu skupih operacija, i razmotrite FlashList v2 s estimatedItemSize za liste koje crpe iz baze.

Koju lokalnu bazu odabrati za 2026.

Pragmatični framework za odluku:

  • Standardna Expo aplikacija s <50k zapisa: expo-sqlite + Drizzle ORM. Tipovi, migracije, reaktivni upiti i nula vendor lock-ina.
  • Offline-first aplikacija s 100k+ zapisa ili sync potrebama: WatermelonDB. Observable sloj i ugrađen sync vrijede dodatnog learning curvea.
  • Bare React Native projekt: OP-SQLite, opcionalno s Drizzle. Maksimalna performansa i kontrola.
  • Samo ključ-vrijednost (postavke, JWT tokeni, feature flagovi): MMKV. Ne pretjerujte sa SQL-om ako vam je dovoljan plain KV store.

U svim slučajevima izbjegavajte AsyncStorage za bilo što veće od nekoliko KB. Sporo je, ne podržava transakcije i serijalizira sve preko JSON-a. Ako naslijedite kod s AsyncStorageom, MMKV je drop-in zamjena za KV slučajeve, a SQLite za sve ostalo.

Često postavljana pitanja

Je li expo-sqlite brži od OP-SQLite u 2026.?

U najnovijim verzijama oba koriste JSI i C++ binding pa su performanse vrlo bliske. Razlika je obično ispod 30% u korist OP-SQLite-a za bulk operacije. Za većinu aplikacija expo-sqlite je dovoljno brz i ima prednost integracije s Expo modulima poput dev clienta i EAS Build pipelinea.

Mogu li koristiti Drizzle ORM s WatermelonDB?

Ne. WatermelonDB ima vlastiti ORM sloj s decorator sintaksom i ne izlaže direktan SQL pristup koji Drizzle treba. Ako želite tipovi-prvo iskustvo, ostanite na expo-sqlite + Drizzle ili OP-SQLite + Drizzle.

Trebam li WAL mod uključiti ručno u expo-sqlite?

Da. Pokrenite PRAGMA journal_mode = 'wal' kao prvu naredbu nakon otvaranja baze. Bez WAL-a, čitanje i pisanje se međusobno blokiraju, što uzrokuje vidljive frame dropove kad u pozadini sinkronizirate podatke dok korisnik scrolla.

Kako enkriptirati lokalnu SQLite bazu u React Native?

OP-SQLite ima ugrađenu SQLCipher integraciju (proslijedite encryptionKey u open()). Za expo-sqlite koristite expo-sqlite/next s SQLITE_HAS_CODEC custom buildom, ili enkriptirajte samo osjetljiva polja kroz expo-crypto prije insertanja. Ključ uvijek čuvajte u expo-secure-store, nikada u kodu.

Trebam li MMKV ako već koristim SQLite?

Često da. MMKV je 20–30× brži od SQLite-a za jednostavne KV operacije (postavke, last-active timestamp, theme preference) jer ne plaća SQL parsing trošak. Tipičan stack u 2026. je SQLite za relacijske podatke i MMKV za session state i postavke, pa se koriste paralelno, ne kao alternativa.

O Autoru Devon Nakashima

Devon is a principal engineer who has been writing React Native since the 0.40 days, with fifteen years total in mobile and web. He led the rewrite of the Wealthsimple trading app from native iOS/Android to a shared RN codebase, then spent two years at Discord on the mobile experience team working on the New Architecture migration and the Hermes upgrade that shipped to 200M+ installs. These days he's an independent consultant and contracts mostly with healthtech and developer-tools companies. He's an occasional contributor to React Navigation and was a maintainer on react-native-mmkv for about a year. His writing here is opinionated and tends toward architecture-level decisions: when to drop down to native modules, how to structure feature flags across iOS and Android, and why he thinks Expo's prebuild model finally won the bare-vs-managed debate around 2024.