On this page
🗄️ Database Guide
Index
Database Options
Local Storage (AsyncStorage)
Local SQLite
Supabase (PostgreSQL)
Firebase Firestore
Custom Backend with Prisma
Sync Patterns
Migrations and Schemas
Database Options
Option Type Best for Pros Cons AsyncStorage Local (Key-Value) Preferences, simple cache Simple, included Not relational, size limit SQLite Local (SQL) Offline-first apps, structured data Relational, fast, offline Local only, no sync Supabase Cloud (PostgreSQL) Apps with users, real-time SQL, built-in Auth, real-time Requires connection Firebase Firestore Cloud (NoSQL) Simple apps, real-time Easy setup, scalable Limited queries, costly at scale Backend + Prisma Cloud (SQL) Full control, complex apps Flexible, typed More development
Quick Decision
Do you need sync across devices?
├── NO → AsyncStorage or SQLite
└── YES → Do you need complex SQL queries?
├── YES → Supabase or custom backend
└── NO → Firebase Firestore
Local Storage (AsyncStorage)
Instalación
npx expo install @react-native-async-storage/async-storage
Uso Básico
import AsyncStorage from '@react-native-async-storage/async-storage' ;
await AsyncStorage. setItem ( 'user_token' , 'abc123' ) ;
await AsyncStorage. setItem ( 'user_preferences' , JSON . stringify ( {
theme: 'dark' ,
language: 'es' ,
notifications: true ,
} ) ) ;
const token = await AsyncStorage. getItem ( 'user_token' ) ;
const prefsString = await AsyncStorage. getItem ( 'user_preferences' ) ;
const prefs = prefsString ? JSON . parse ( prefsString) : null ;
await AsyncStorage. removeItem ( 'user_token' ) ;
await AsyncStorage. multiRemove ( [ 'key1' , 'key2' , 'key3' ] ) ;
await AsyncStorage. clear ( ) ;
const keys = await AsyncStorage. getAllKeys ( ) ;
Hook Personalizado para AsyncStorage
import { useState, useEffect, useCallback } from 'react' ;
import AsyncStorage from '@react-native-async-storage/async-storage' ;
export function useAsyncStorage < T > ( key: string , initialValue: T ) {
const [ storedValue, setStoredValue] = useState < T > ( initialValue) ;
const [ isLoading, setIsLoading] = useState ( true ) ;
useEffect ( ( ) => {
const loadValue = async ( ) => {
try {
const item = await AsyncStorage. getItem ( key) ;
if ( item !== null ) {
setStoredValue ( JSON . parse ( item) ) ;
}
} catch ( error) {
console . error ( 'Error loading from AsyncStorage:' , error) ;
} finally {
setIsLoading ( false ) ;
}
} ;
loadValue ( ) ;
} , [ key] ) ;
const setValue = useCallback ( async ( value: T | ( ( prev: T ) => T ) ) => {
try {
const valueToStore = value instanceof Function ? value ( storedValue) : value;
setStoredValue ( valueToStore) ;
await AsyncStorage. setItem ( key, JSON . stringify ( valueToStore) ) ;
} catch ( error) {
console . error ( 'Error saving to AsyncStorage:' , error) ;
}
} , [ key, storedValue] ) ;
const removeValue = useCallback ( async ( ) => {
try {
await AsyncStorage. removeItem ( key) ;
setStoredValue ( initialValue) ;
} catch ( error) {
console . error ( 'Error removing from AsyncStorage:' , error) ;
}
} , [ key, initialValue] ) ;
return { value: storedValue, setValue, removeValue, isLoading } ;
}
Integración con React Query
import createContextHook from '@nkzw/create-context-hook' ;
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' ;
import AsyncStorage from '@react-native-async-storage/async-storage' ;
interface Todo {
id: string ;
title: string ;
completed: boolean ;
createdAt: string ;
}
const STORAGE_KEY = 'todos' ;
export const [ TodoProvider, useTodos] = createContextHook ( ( ) => {
const queryClient = useQueryClient ( ) ;
const todosQuery = useQuery ( {
queryKey: [ 'todos' ] ,
queryFn: async ( ) : Promise < Todo[ ] > => {
const stored = await AsyncStorage. getItem ( STORAGE_KEY ) ;
return stored ? JSON . parse ( stored) : [ ] ;
} ,
} ) ;
const saveMutation = useMutation ( {
mutationFn : async ( todos: Todo[ ] ) => {
await AsyncStorage. setItem ( STORAGE_KEY , JSON . stringify ( todos) ) ;
return todos;
} ,
onSuccess : ( data) => {
queryClient. setQueryData ( [ 'todos' ] , data) ;
} ,
} ) ;
const addTodo = ( title: string ) => {
const newTodo: Todo = {
id: Date. now ( ) . toString ( ) ,
title,
completed: false ,
createdAt: new Date ( ) . toISOString ( ) ,
} ;
const updated = [ ... ( todosQuery. data || [ ] ) , newTodo] ;
saveMutation. mutate ( updated) ;
} ;
const toggleTodo = ( id: string ) => {
const updated = ( todosQuery. data || [ ] ) . map ( todo =>
todo. id === id ? { ... todo, completed: ! todo. completed } : todo
) ;
saveMutation. mutate ( updated) ;
} ;
const deleteTodo = ( id: string ) => {
const updated = ( todosQuery. data || [ ] ) . filter ( todo => todo. id !== id) ;
saveMutation. mutate ( updated) ;
} ;
return {
todos: todosQuery. data || [ ] ,
isLoading: todosQuery. isLoading,
addTodo,
toggleTodo,
deleteTodo,
} ;
} ) ;
SQLite Local
Instalación
npx expo install expo-sqlite
Configuración y Uso
import * as SQLite from 'expo-sqlite' ;
const db = SQLite. openDatabaseSync ( 'myapp.db' ) ;
export async function initDatabase ( ) {
await db. execAsync ( `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
avatar TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
completed INTEGER DEFAULT 0,
due_date DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
);
CREATE INDEX IF NOT EXISTS idx_todos_user_id ON todos (user_id);
` ) ;
}
export const todosDB = {
async getAll ( userId: number ) {
return db. getAllAsync < Todo> (
'SELECT * FROM todos WHERE user_id = ? ORDER BY created_at DESC' ,
[ userId]
) ;
} ,
async getById ( id: number ) {
return db. getFirstAsync < Todo> (
'SELECT * FROM todos WHERE id = ?' ,
[ id]
) ;
} ,
async create ( todo: Omit< Todo, 'id' | 'created_at' > ) {
const result = await db. runAsync (
'INSERT INTO todos (user_id, title, description, completed, due_date) VALUES (?, ?, ?, ?, ?)' ,
[ todo. user_id, todo. title, todo. description || null , todo. completed ? 1 : 0 , todo. due_date || null ]
) ;
return result. lastInsertRowId;
} ,
async update ( id: number , updates: Partial< Todo> ) {
const fields = Object. keys ( updates)
. map ( key => ` ${ key} = ? ` )
. join ( ', ' ) ;
const values = Object. values ( updates) ;
await db. runAsync (
` UPDATE todos SET ${ fields} WHERE id = ? ` ,
[ ... values, id]
) ;
} ,
async delete ( id: number ) {
await db. runAsync ( 'DELETE FROM todos WHERE id = ?' , [ id] ) ;
} ,
async toggleCompleted ( id: number ) {
await db. runAsync (
'UPDATE todos SET completed = NOT completed WHERE id = ?' ,
[ id]
) ;
} ,
} ;
Hook para SQLite con React Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' ;
import { todosDB } from '@/lib/database' ;
export function useSQLiteTodos ( userId: number ) {
const queryClient = useQueryClient ( ) ;
const todosQuery = useQuery ( {
queryKey: [ 'todos' , userId] ,
queryFn : ( ) => todosDB. getAll ( userId) ,
} ) ;
const createMutation = useMutation ( {
mutationFn: ( todo: Omit< Todo, 'id' | 'created_at' > ) => todosDB. create ( todo) ,
onSuccess : ( ) => {
queryClient. invalidateQueries ( { queryKey: [ 'todos' , userId] } ) ;
} ,
} ) ;
const updateMutation = useMutation ( {
mutationFn : ( { id, updates } : { id: number ; updates: Partial< Todo> } ) =>
todosDB. update ( id, updates) ,
onSuccess : ( ) => {
queryClient. invalidateQueries ( { queryKey: [ 'todos' , userId] } ) ;
} ,
} ) ;
const deleteMutation = useMutation ( {
mutationFn : ( id: number ) => todosDB. delete ( id) ,
onSuccess : ( ) => {
queryClient. invalidateQueries ( { queryKey: [ 'todos' , userId] } ) ;
} ,
} ) ;
return {
todos: todosQuery. data || [ ] ,
isLoading: todosQuery. isLoading,
create: createMutation. mutate,
update: updateMutation. mutate,
delete : deleteMutation. mutate,
isCreating: createMutation. isPending,
} ;
}
Supabase (PostgreSQL)
Instalación
npx expo install @supabase/supabase-js
Configuración
import 'react-native-url-polyfill/auto' ;
import { createClient } from '@supabase/supabase-js' ;
import AsyncStorage from '@react-native-async-storage/async-storage' ;
const supabaseUrl = process. env. EXPO_PUBLIC_SUPABASE_URL ! ;
const supabaseAnonKey = process. env. EXPO_PUBLIC_SUPABASE_ANON_KEY ! ;
export const supabase = createClient ( supabaseUrl, supabaseAnonKey, {
auth: {
storage: AsyncStorage,
autoRefreshToken: true ,
persistSession: true ,
detectSessionInUrl: false ,
} ,
} ) ;
export interface Database {
public : {
Tables: {
profiles: {
Row: {
id: string ;
email: string ;
name: string ;
avatar_url: string | null ;
created_at: string ;
} ;
Insert: Omit< Database[ 'public' ] [ 'Tables' ] [ 'profiles' ] [ 'Row' ] , 'created_at' > ;
Update: Partial< Database[ 'public' ] [ 'Tables' ] [ 'profiles' ] [ 'Insert' ] > ;
} ;
todos: {
Row: {
id: number ;
user_id: string ;
title: string ;
description: string | null ;
completed: boolean ;
due_date: string | null ;
created_at: string ;
} ;
Insert: Omit< Database[ 'public' ] [ 'Tables' ] [ 'todos' ] [ 'Row' ] , 'id' | 'created_at' > ;
Update: Partial< Database[ 'public' ] [ 'Tables' ] [ 'todos' ] [ 'Insert' ] > ;
} ;
} ;
} ;
}
Operaciones CRUD con Supabase
import { supabase, Database } from './supabase' ;
type Todo = Database[ 'public' ] [ 'Tables' ] [ 'todos' ] [ 'Row' ] ;
type TodoInsert = Database[ 'public' ] [ 'Tables' ] [ 'todos' ] [ 'Insert' ] ;
type TodoUpdate = Database[ 'public' ] [ 'Tables' ] [ 'todos' ] [ 'Update' ] ;
export const supabaseTodos = {
async getAll ( userId: string ) : Promise < Todo[ ] > {
const { data, error } = await supabase
. from ( 'todos' )
. select ( '*' )
. eq ( 'user_id' , userId)
. order ( 'created_at' , { ascending: false } ) ;
if ( error) throw error;
return data || [ ] ;
} ,
async getById ( id: number ) : Promise < Todo | null > {
const { data, error } = await supabase
. from ( 'todos' )
. select ( '*' )
. eq ( 'id' , id)
. single ( ) ;
if ( error) throw error;
return data;
} ,
async create ( todo: TodoInsert) : Promise < Todo> {
const { data, error } = await supabase
. from ( 'todos' )
. insert ( todo)
. select ( )
. single ( ) ;
if ( error) throw error;
return data;
} ,
async update ( id: number , updates: TodoUpdate) : Promise < Todo> {
const { data, error } = await supabase
. from ( 'todos' )
. update ( updates)
. eq ( 'id' , id)
. select ( )
. single ( ) ;
if ( error) throw error;
return data;
} ,
async delete ( id: number ) : Promise < void > {
const { error } = await supabase
. from ( 'todos' )
. delete ( )
. eq ( 'id' , id) ;
if ( error) throw error;
} ,
} ;
Suscripciones en Tiempo Real
import { useEffect } from 'react' ;
import { useQuery, useQueryClient } from '@tanstack/react-query' ;
import { supabase } from '@/lib/supabase' ;
import { supabaseTodos } from '@/lib/supabase-db' ;
export function useRealtimeTodos ( userId: string ) {
const queryClient = useQueryClient ( ) ;
const todosQuery = useQuery ( {
queryKey: [ 'todos' , userId] ,
queryFn : ( ) => supabaseTodos. getAll ( userId) ,
} ) ;
useEffect ( ( ) => {
const channel = supabase
. channel ( 'todos-changes' )
. on (
'postgres_changes' ,
{
event: '*' ,
schema: 'public' ,
table: 'todos' ,
filter: ` user_id=eq. ${ userId} ` ,
} ,
( payload) => {
console . log ( 'Cambio recibido:' , payload) ;
queryClient. invalidateQueries ( { queryKey: [ 'todos' , userId] } ) ;
}
)
. subscribe ( ) ;
return ( ) => {
supabase. removeChannel ( channel) ;
} ;
} , [ userId, queryClient] ) ;
return todosQuery;
}
Contexto Completo con Supabase
import createContextHook from '@nkzw/create-context-hook' ;
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' ;
import { useEffect } from 'react' ;
import { supabase } from '@/lib/supabase' ;
import { supabaseTodos } from '@/lib/supabase-db' ;
import { useAuth } from './AuthContext' ;
export const [ SupabaseDataProvider, useSupabaseData] = createContextHook ( ( ) => {
const queryClient = useQueryClient ( ) ;
const { user } = useAuth ( ) ;
const userId = user?. id;
const todosQuery = useQuery ( {
queryKey: [ 'todos' , userId] ,
queryFn : ( ) => supabaseTodos. getAll ( userId! ) ,
enabled: ! ! userId,
} ) ;
useEffect ( ( ) => {
if ( ! userId) return ;
const channel = supabase
. channel ( 'todos-realtime' )
. on (
'postgres_changes' ,
{
event: '*' ,
schema: 'public' ,
table: 'todos' ,
filter: ` user_id=eq. ${ userId} ` ,
} ,
( ) => {
queryClient. invalidateQueries ( { queryKey: [ 'todos' , userId] } ) ;
}
)
. subscribe ( ) ;
return ( ) => {
supabase. removeChannel ( channel) ;
} ;
} , [ userId, queryClient] ) ;
const createMutation = useMutation ( {
mutationFn : ( data: { title: string ; description? : string } ) =>
supabaseTodos. create ( {
user_id: userId! ,
title: data. title,
description: data. description || null ,
completed: false ,
due_date: null ,
} ) ,
onSuccess : ( ) => {
queryClient. invalidateQueries ( { queryKey: [ 'todos' , userId] } ) ;
} ,
} ) ;
const updateMutation = useMutation ( {
mutationFn : ( { id, updates } : { id: number ; updates: any } ) =>
supabaseTodos. update ( id, updates) ,
onSuccess : ( ) => {
queryClient. invalidateQueries ( { queryKey: [ 'todos' , userId] } ) ;
} ,
} ) ;
const deleteMutation = useMutation ( {
mutationFn : ( id: number ) => supabaseTodos. delete ( id) ,
onSuccess : ( ) => {
queryClient. invalidateQueries ( { queryKey: [ 'todos' , userId] } ) ;
} ,
} ) ;
return {
todos: todosQuery. data || [ ] ,
isLoading: todosQuery. isLoading,
error: todosQuery. error,
createTodo: createMutation. mutateAsync,
updateTodo: updateMutation. mutateAsync,
deleteTodo: deleteMutation. mutateAsync,
isCreating: createMutation. isPending,
isUpdating: updateMutation. isPending,
isDeleting: deleteMutation. isPending,
} ;
} ) ;
Firebase Firestore
Instalación
npx expo install firebase
Configuración
import { initializeApp } from 'firebase/app' ;
import { getFirestore } from 'firebase/firestore' ;
const firebaseConfig = {
apiKey: process. env. EXPO_PUBLIC_FIREBASE_API_KEY ,
authDomain: process. env. EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN ,
projectId: process. env. EXPO_PUBLIC_FIREBASE_PROJECT_ID ,
storageBucket: process. env. EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET ,
messagingSenderId: process. env. EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID ,
appId: process. env. EXPO_PUBLIC_FIREBASE_APP_ID ,
} ;
const app = initializeApp ( firebaseConfig) ;
export const db = getFirestore ( app) ;
Operaciones CRUD con Firestore
import {
collection,
doc,
getDocs,
getDoc,
addDoc,
updateDoc,
deleteDoc,
query,
where,
orderBy,
onSnapshot,
Timestamp,
} from 'firebase/firestore' ;
import { db } from './firebase' ;
interface Todo {
id: string ;
userId: string ;
title: string ;
description? : string ;
completed: boolean ;
dueDate? : Date;
createdAt: Date;
}
const todosCollection = collection ( db, 'todos' ) ;
export const firestoreTodos = {
async getAll ( userId: string ) : Promise < Todo[ ] > {
const q = query (
todosCollection,
where ( 'userId' , '==' , userId) ,
orderBy ( 'createdAt' , 'desc' )
) ;
const snapshot = await getDocs ( q) ;
return snapshot. docs. map ( doc => ( {
id: doc. id,
... doc. data ( ) ,
createdAt: doc. data ( ) . createdAt?. toDate ( ) ,
dueDate: doc. data ( ) . dueDate?. toDate ( ) ,
} ) ) as Todo[ ] ;
} ,
async getById ( id: string ) : Promise < Todo | null > {
const docRef = doc ( db, 'todos' , id) ;
const docSnap = await getDoc ( docRef) ;
if ( ! docSnap. exists ( ) ) return null ;
return {
id: docSnap. id,
... docSnap. data ( ) ,
createdAt: docSnap. data ( ) . createdAt?. toDate ( ) ,
dueDate: docSnap. data ( ) . dueDate?. toDate ( ) ,
} as Todo;
} ,
async create ( todo: Omit< Todo, 'id' | 'createdAt' > ) : Promise < string > {
const docRef = await addDoc ( todosCollection, {
... todo,
createdAt: Timestamp. now ( ) ,
dueDate: todo. dueDate ? Timestamp. fromDate ( todo. dueDate) : null ,
} ) ;
return docRef. id;
} ,
async update ( id: string , updates: Partial< Todo> ) : Promise < void > {
const docRef = doc ( db, 'todos' , id) ;
const updateData = { ... updates } ;
if ( updates. dueDate) {
updateData. dueDate = Timestamp. fromDate ( updates. dueDate) as any ;
}
await updateDoc ( docRef, updateData) ;
} ,
async delete ( id: string ) : Promise < void > {
const docRef = doc ( db, 'todos' , id) ;
await deleteDoc ( docRef) ;
} ,
subscribe ( userId: string , callback : ( todos: Todo[ ] ) => void ) {
const q = query (
todosCollection,
where ( 'userId' , '==' , userId) ,
orderBy ( 'createdAt' , 'desc' )
) ;
return onSnapshot ( q, ( snapshot) => {
const todos = snapshot. docs. map ( doc => ( {
id: doc. id,
... doc. data ( ) ,
createdAt: doc. data ( ) . createdAt?. toDate ( ) ,
dueDate: doc. data ( ) . dueDate?. toDate ( ) ,
} ) ) as Todo[ ] ;
callback ( todos) ;
} ) ;
} ,
} ;
Hook con Firestore y Tiempo Real
import { useState, useEffect } from 'react' ;
import { useMutation, useQueryClient } from '@tanstack/react-query' ;
import { firestoreTodos } from '@/lib/firestore-db' ;
export function useFirestoreTodos ( userId: string ) {
const queryClient = useQueryClient ( ) ;
const [ todos, setTodos] = useState < Todo[ ] > ( [ ] ) ;
const [ isLoading, setIsLoading] = useState ( true ) ;
useEffect ( ( ) => {
const unsubscribe = firestoreTodos. subscribe ( userId, ( newTodos) => {
setTodos ( newTodos) ;
setIsLoading ( false ) ;
queryClient. setQueryData ( [ 'todos' , userId] , newTodos) ;
} ) ;
return ( ) => unsubscribe ( ) ;
} , [ userId, queryClient] ) ;
const createMutation = useMutation ( {
mutationFn : ( data: { title: string ; description? : string } ) =>
firestoreTodos. create ( {
userId,
title: data. title,
description: data. description,
completed: false ,
} ) ,
} ) ;
const updateMutation = useMutation ( {
mutationFn : ( { id, updates } : { id: string ; updates: Partial< Todo> } ) =>
firestoreTodos. update ( id, updates) ,
} ) ;
const deleteMutation = useMutation ( {
mutationFn : ( id: string ) => firestoreTodos. delete ( id) ,
} ) ;
return {
todos,
isLoading,
create: createMutation. mutateAsync,
update: updateMutation. mutateAsync,
delete : deleteMutation. mutateAsync,
} ;
}
Backend Propio con Prisma
Estructura
backend/
├── prisma/
│ └── schema.prisma
├── trpc/
│ ├── todos/
│ │ ├── list/route.ts
│ │ ├── create/route.ts
│ │ ├── update/route.ts
│ │ └── delete/route.ts
│ └── app-router.ts
└── lib/
└── prisma.ts
Schema de Prisma
// backend/prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String
avatar String?
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
todos Todo[]
sessions Session[]
}
model Session {
id String @id @default(cuid())
userId String
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Todo {
id String @id @default(cuid())
userId String
title String
description String?
completed Boolean @default(false)
dueDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
Procedimientos tRPC
import { z } from 'zod' ;
import { protectedProcedure } from '../../trpc' ;
export const listTodosProcedure = protectedProcedure
. input ( z. object ( {
completed: z. boolean ( ) . optional ( ) ,
limit: z. number ( ) . min ( 1 ) . max ( 100 ) . default ( 50 ) ,
cursor: z. string ( ) . optional ( ) ,
} ) . optional ( ) )
. query ( async ( { ctx, input } ) => {
const { completed, limit, cursor } = input || { } ;
const todos = await ctx. db. todo. findMany ( {
where: {
userId: ctx. userId,
... ( completed !== undefined && { completed } ) ,
} ,
take: limit + 1 ,
cursor: cursor ? { id: cursor } : undefined ,
orderBy: { createdAt: 'desc' } ,
} ) ;
let nextCursor: string | undefined ;
if ( todos. length > limit) {
const nextItem = todos. pop ( ) ;
nextCursor = nextItem?. id;
}
return {
items: todos,
nextCursor,
} ;
} ) ;
import { z } from 'zod' ;
import { protectedProcedure } from '../../trpc' ;
export const createTodoProcedure = protectedProcedure
. input ( z. object ( {
title: z. string ( ) . min ( 1 ) . max ( 200 ) ,
description: z. string ( ) . max ( 1000 ) . optional ( ) ,
dueDate: z. string ( ) . datetime ( ) . optional ( ) ,
} ) )
. mutation ( async ( { ctx, input } ) => {
const todo = await ctx. db. todo. create ( {
data: {
userId: ctx. userId,
title: input. title,
description: input. description,
dueDate: input. dueDate ? new Date ( input. dueDate) : null ,
} ,
} ) ;
return todo;
} ) ;
Uso en el Cliente
import { trpc } from '@/lib/trpc' ;
function TodoList ( ) {
const todosQuery = trpc. todos. list. useQuery ( { limit: 20 } ) ;
const createMutation = trpc. todos. create. useMutation ( {
onSuccess : ( ) => {
todosQuery. refetch ( ) ;
} ,
} ) ;
const handleCreate = ( ) => {
createMutation. mutate ( {
title: 'Nuevo todo' ,
description: 'Descripción opcional' ,
} ) ;
} ;
if ( todosQuery. isLoading) return < Loading / > ;
if ( todosQuery. error) return < Error message= { todosQuery. error. message} / > ;
return (
< FlatList
data= { todosQuery. data?. items}
renderItem= { ( { item } ) => < TodoItem todo= { item} / > }
/ >
) ;
}
Patrones de Sincronización
Offline-First con Cola de Sincronización
import AsyncStorage from '@react-native-async-storage/async-storage' ;
import NetInfo from '@react-native-community/netinfo' ;
interface SyncOperation {
id: string ;
type: 'create' | 'update' | 'delete' ;
table: string ;
data: any ;
timestamp: number ;
}
const QUEUE_KEY = 'sync_queue' ;
export const syncQueue = {
async add ( operation: Omit< SyncOperation, 'id' | 'timestamp' > ) {
const queue = await this . getQueue ( ) ;
queue. push ( {
... operation,
id: Date. now ( ) . toString ( ) ,
timestamp: Date. now ( ) ,
} ) ;
await AsyncStorage. setItem ( QUEUE_KEY , JSON . stringify ( queue) ) ;
} ,
async getQueue ( ) : Promise < SyncOperation[ ] > {
const stored = await AsyncStorage. getItem ( QUEUE_KEY ) ;
return stored ? JSON . parse ( stored) : [ ] ;
} ,
async remove ( id: string ) {
const queue = await this . getQueue ( ) ;
const updated = queue. filter ( op => op. id !== id) ;
await AsyncStorage. setItem ( QUEUE_KEY , JSON . stringify ( updated) ) ;
} ,
async processQueue ( syncFn : ( op: SyncOperation) => Promise < void > ) {
const isConnected = await NetInfo. fetch ( ) . then ( state => state. isConnected) ;
if ( ! isConnected) return ;
const queue = await this . getQueue ( ) ;
for ( const operation of queue) {
try {
await syncFn ( operation) ;
await this . remove ( operation. id) ;
} catch ( error) {
console . error ( 'Sync failed for operation:' , operation. id, error) ;
break ;
}
}
} ,
} ;
Hook de Sincronización
import { useEffect } from 'react' ;
import { useQueryClient } from '@tanstack/react-query' ;
import NetInfo from '@react-native-community/netinfo' ;
import { syncQueue } from '@/lib/syncQueue' ;
export function useSyncedData ( ) {
const queryClient = useQueryClient ( ) ;
useEffect ( ( ) => {
const unsubscribe = NetInfo. addEventListener ( state => {
if ( state. isConnected) {
syncQueue. processQueue ( async ( operation) => {
switch ( operation. type) {
case 'create' :
await api. create ( operation. table, operation. data) ;
break ;
case 'update' :
await api. update ( operation. table, operation. data. id, operation. data) ;
break ;
case 'delete' :
await api. delete ( operation. table, operation. data. id) ;
break ;
}
} ) ;
queryClient. invalidateQueries ( ) ;
}
} ) ;
return ( ) => unsubscribe ( ) ;
} , [ queryClient] ) ;
}
Migraciones y Esquemas
Supabase Migrations
CREATE TABLE profiles (
id UUID PRIMARY KEY REFERENCES auth. users( id) ON DELETE CASCADE ,
email TEXT UNIQUE NOT NULL ,
name TEXT NOT NULL ,
avatar_url TEXT ,
created_at TIMESTAMPTZ DEFAULT NOW ( )
) ;
CREATE TABLE todos (
id SERIAL PRIMARY KEY ,
user_id UUID NOT NULL REFERENCES profiles( id) ON DELETE CASCADE ,
title TEXT NOT NULL ,
description TEXT ,
completed BOOLEAN DEFAULT FALSE ,
due_date TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW ( )
) ;
CREATE INDEX idx_todos_user_id ON todos( user_id) ;
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING ( auth. uid( ) = id) ;
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING ( auth. uid( ) = id) ;
CREATE POLICY "Users can view own todos"
ON todos FOR SELECT
USING ( auth. uid( ) = user_id) ;
CREATE POLICY "Users can create own todos"
ON todos FOR INSERT
WITH CHECK ( auth. uid( ) = user_id) ;
CREATE POLICY "Users can update own todos"
ON todos FOR UPDATE
USING ( auth. uid( ) = user_id) ;
CREATE POLICY "Users can delete own todos"
ON todos FOR DELETE
USING ( auth. uid( ) = user_id) ;
Checklist de Implementación