logo-text
DatabasePerformanceReact Native

Master WatermelonDB with React Native: The Ultimate High-Performance Database Guide 2025 πŸ‰

By Viewlytics Team18 min readMay 31, 2025

WatermelonDB is a revolutionary reactive database framework that enables React Native apps to scale from hundreds to tens of thousands of records while maintaining lightning-fast performance. This comprehensive guide covers everything from setup to advanced optimization techniques.

1. What is WatermelonDB and Why You Need It

WatermelonDB is a next-generation reactive database framework built specifically for React and React Native applications. Unlike traditional solutions like Redux with persistence or AsyncStorage, WatermelonDB is designed to handle complex applications with thousands of records without sacrificing performance.

πŸ‰ Key Benefits of WatermelonDB:
  • ⚑ Instant Launch: Lazy loading means your app starts fast regardless of data size
  • πŸ“ˆ Scalable: Handles tens of thousands of records efficiently
  • πŸ”„ Reactive: UI automatically updates when data changes
  • πŸ”Œ Offline-first: Built-in sync capabilities for offline support
  • 🎯 SQLite Foundation: Rock-solid database engine underneath
  • πŸ“± Multi-platform: iOS, Android, Web, Windows, macOS support

When to Choose WatermelonDB

WatermelonDB is perfect for:

  • Complex apps with large datasets: E-commerce, social media, productivity apps
  • Offline-capable applications: Note-taking, CRM, inventory management
  • Real-time collaborative apps: Chat applications, project management tools
  • Performance-critical apps: When app launch speed is crucial
πŸ’‘ Performance Comparison: Apps using Redux with large datasets can take 2-5 seconds to load on slower devices. WatermelonDB apps launch instantly because data is loaded on-demand, not upfront.

2. Installation and Project Setup

Step 1: Install WatermelonDB

First, install WatermelonDB and its required dependencies:

# Install WatermelonDB
npm install @nozbe/watermelondb

# Install Babel plugin for decorators
npm install --save-dev @babel/plugin-proposal-decorators

# For iOS projects
cd ios && npx pod-install

Step 2: Configure Babel

Add decorator support to your .babelrc or babel.config.js:

// babel.config.js
module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
    ['@babel/plugin-proposal-decorators', { legacy: true }]
  ]
};

Step 3: iOS Configuration (React Native 0.60+)

For iOS, update your Podfile:

# Add to your Podfile
pod 'WatermelonDB', path: '../node_modules/@nozbe/watermelondb'
pod 'React-jsi', path: '../node_modules/react-native/ReactCommon/jsi', modular_headers: true
pod 'simdjson', path: '../node_modules/@nozbe/simdjson', modular_headers: true

Then run npx pod-install to install the native dependencies.

3. Defining Your Database Schema

WatermelonDB uses a schema-first approach. You define your database structure using schemas, which are then used to generate the underlying SQLite tables.

Creating a Basic Schema

Create a model/schema.js file:

// model/schema.js
import { appSchema, tableSchema } from '@nozbe/watermelondb'

export const mySchema = appSchema({
  version: 1,
  tables: [
    tableSchema({
      name: 'posts',
      columns: [
        { name: 'title', type: 'string' },
        { name: 'body', type: 'string' },
        { name: 'is_pinned', type: 'boolean' },
        { name: 'created_at', type: 'number' },
        { name: 'updated_at', type: 'number' }
      ]
    }),
    tableSchema({
      name: 'comments',
      columns: [
        { name: 'body', type: 'string' },
        { name: 'post_id', type: 'string', isIndexed: true },
        { name: 'author_name', type: 'string' },
        { name: 'created_at', type: 'number' },
        { name: 'updated_at', type: 'number' }
      ]
    }),
    tableSchema({
      name: 'users',
      columns: [
        { name: 'name', type: 'string' },
        { name: 'email', type: 'string', isIndexed: true },
        { name: 'avatar_url', type: 'string', isOptional: true },
        { name: 'is_verified', type: 'boolean' },
        { name: 'last_seen_at', type: 'number', isOptional: true },
        { name: 'created_at', type: 'number' },
        { name: 'updated_at', type: 'number' }
      ]
    })
  ]
})
⚠️ Schema Naming Conventions:
  • Table names: plural, snake_case (e.g., blog_posts)
  • Column names: snake_case (e.g., created_at)
  • Relation columns: end with _id (e.g., post_id)
  • Boolean columns: start with is_ (e.g., is_verified)
  • Date columns: end with _at (e.g., updated_at)

Column Types and Indexing

WatermelonDB supports three column types:

// Column types example
{
  // String columns (default: '')
  { name: 'title', type: 'string' },
  { name: 'description', type: 'string', isOptional: true }, // Can be null
  
  // Number columns (default: 0) - used for integers, floats, timestamps
  { name: 'price', type: 'number' },
  { name: 'created_at', type: 'number' },
  
  // Boolean columns (default: false)
  { name: 'is_featured', type: 'boolean' },
  
  // Indexed columns for faster queries
  { name: 'category_id', type: 'string', isIndexed: true },
  { name: 'email', type: 'string', isIndexed: true }
}
🎯 Indexing Best Practices:
  • Index columns you frequently query by (especially _id fields)
  • Index boolean fields if you often filter by them
  • Avoid indexing long text fields or rarely queried columns
  • Remember: indexes speed up queries but slow down writes

4. Creating Models

Models are JavaScript classes that represent your data entities. They define the structure, relationships, and behavior of your data.

Basic Model Definition

Create model files for each table:

// model/Post.js
import { Model } from '@nozbe/watermelondb'
import { field, date, children, readonly } from '@nozbe/watermelondb/decorators'

export default class Post extends Model {
  static table = 'posts'

  @field('title') title
  @field('body') body
  @field('is_pinned') isPinned
  @date('created_at') createdAt
  @date('updated_at') updatedAt
  
  // Relationship: a post has many comments
  @children('comments') comments
}

// model/Comment.js
import { Model } from '@nozbe/watermelondb'
import { field, date, relation } from '@nozbe/watermelondb/decorators'

export default class Comment extends Model {
  static table = 'comments'

  @field('body') body
  @field('author_name') authorName
  @date('created_at') createdAt
  @date('updated_at') updatedAt
  
  // Relationship: a comment belongs to a post
  @relation('posts', 'post_id') post
}

// model/User.js
import { Model } from '@nozbe/watermelondb'
import { field, date } from '@nozbe/watermelondb/decorators'

export default class User extends Model {
  static table = 'users'

  @field('name') name
  @field('email') email
  @field('avatar_url') avatarUrl
  @field('is_verified') isVerified
  @date('last_seen_at') lastSeenAt
  @date('created_at') createdAt
  @date('updated_at') updatedAt
}

Advanced Model Features

Add computed properties and custom methods to your models:

// Enhanced Post model
import { Model, Q } from '@nozbe/watermelondb'
import { field, date, children, lazy } from '@nozbe/watermelondb/decorators'

export default class Post extends Model {
  static table = 'posts'

  @field('title') title
  @field('body') body
  @field('is_pinned') isPinned
  @date('created_at') createdAt
  @date('updated_at') updatedAt
  
  @children('comments') comments
  
  // Computed property
  get excerpt() {
    return this.body.substring(0, 100) + '...'
  }
  
  // Lazy associations - loaded only when accessed
  @lazy recentComments = this.collections
    .get('comments')
    .query(
      Q.where('post_id', this.id),
      Q.sortBy('created_at', Q.desc),
      Q.take(5)
    )
  
  // Custom methods
  async addComment(body, authorName) {
    return await this.collections.get('comments').create(comment => {
      comment.body = body
      comment.authorName = authorName
      comment.post.set(this)
    })
  }
  
  async togglePin() {
    return await this.update(post => {
      post.isPinned = !post.isPinned
    })
  }
}

5. Database Initialization

Setting Up the Database

Create a database instance and configure your app:

// database/index.js
import { Database } from '@nozbe/watermelondb'
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite'

import { mySchema } from '../model/schema'
import Post from '../model/Post'
import Comment from '../model/Comment'
import User from '../model/User'

// Create adapter
const adapter = new SQLiteAdapter({
  schema: mySchema,
  // (You might want to comment it out for production)
  onSetUpError: error => {
    console.error('Database setup error:', error)
  }
})

// Create database
export const database = new Database({
  adapter,
  modelClasses: [
    Post,
    Comment,
    User,
  ],
})

export default database

Integrating with Your App

Provide the database to your React Native app:

// App.js
import React from 'react'
import { DatabaseProvider } from '@nozbe/watermelondb/DatabaseProvider'
import database from './database'
import MainNavigator from './navigation/MainNavigator'

export default function App() {
  return (
    <DatabaseProvider database={database}>
      <MainNavigator />
    </DatabaseProvider>
  )
}

6. Reactive Components with withObservables

The magic of WatermelonDB lies in its reactive nature. Components automatically re-render when underlying data changes, thanks to the withObservables higher-order component.

Basic Reactive Component

// components/PostList.js
import React from 'react'
import { FlatList } from 'react-native'
import { withDatabase } from '@nozbe/watermelondb/DatabaseProvider'
import { withObservables } from '@nozbe/watermelondb/react'
import { Q } from '@nozbe/watermelondb'

import PostItem from './PostItem'

function PostList(props) {
  const { posts } = props
  
  const renderPost = ({ item }) => (
    <PostItem key={item.id} post={item} />
  )

  return (
    <FlatList
      data={posts}
      renderItem={renderPost}
      keyExtractor={item => item.id}
    />
  )
}

// Make component reactive to database changes
const enhance = withObservables([], ({ database }) => ({
  posts: database.collections.get('posts').query(
    Q.sortBy('created_at', Q.desc)
  )
}))

export default withDatabase(enhance(PostList))

Component with Parameters

// components/PostDetail.js
import React from 'react'
import { View, Text, FlatList } from 'react-native'
import { withObservables } from '@nozbe/watermelondb/react'

function PostDetail(props) {
  const { post, comments } = props

  return (
    <View>
      <Text style={{ fontSize: 24, fontWeight: 'bold' }}>
        {post.title}
      </Text>
      <Text>{post.body}</Text>
      
      <Text style={{ fontSize: 18, marginTop: 20 }}>
        Comments ({comments.length})
      </Text>
      <FlatList
        data={comments}
        renderItem={({ item }) => (
          <View style={{ padding: 10, borderBottomWidth: 1 }}>
            <Text>{item.body}</Text>
            <Text style={{ color: 'gray' }}>β€” {item.authorName}</Text>
          </View>
        )}
        keyExtractor={item => item.id}
      />
    </View>
  )
}

// Reactive component that depends on the post prop
const enhance = withObservables(['post'], ({ post }) => ({
  post, // The post itself is observable
  comments: post.comments // Related comments are also observed
}))

export default enhance(PostDetail)

Using Hooks with useDatabase

For simpler cases, you can use hooks:

// components/CreatePost.js
import React, { useState } from 'react'
import { View, TextInput, TouchableOpacity, Text } from 'react-native'
import { useDatabase } from '@nozbe/watermelondb/hooks'

export default function CreatePost() {
  const [title, setTitle] = useState('')
  const [body, setBody] = useState('')
  const database = useDatabase()

  const createPost = async () => {
    if (!title.trim() || !body.trim()) return

    await database.write(async () => {
      await database.get('posts').create(post => {
        post.title = title
        post.body = body
        post.isPinned = false
      })
    })

    // Clear form
    setTitle('')
    setBody('')
  }

  return (
    <View style={{ padding: 20 }}>
      <TextInput
        placeholder="Post title"
        value={title}
        onChangeText={setTitle}
        style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
      />
      <TextInput
        placeholder="Post body"
        value={body}
        onChangeText={setBody}
        multiline
        style={{ borderWidth: 1, padding: 10, height: 100, marginBottom: 10 }}
      />
      <TouchableOpacity 
        onPress={createPost}
        style={{ backgroundColor: 'blue', padding: 15, borderRadius: 8 }}
      >
        <Text style={{ color: 'white', textAlign: 'center' }}>
          Create Post
        </Text>
      </TouchableOpacity>
    </View>
  )
}

7. Advanced Querying

WatermelonDB provides a powerful query API that lets you efficiently filter, sort, and paginate your data.

Complex Queries

import { Q } from '@nozbe/watermelondb'

// Get all pinned posts sorted by creation date
const pinnedPosts = await database.get('posts').query(
  Q.where('is_pinned', true),
  Q.sortBy('created_at', Q.desc)
).fetch()

// Get posts with specific conditions
const recentPosts = await database.get('posts').query(
  Q.where('created_at', Q.gt(Date.now() - 7 * 24 * 60 * 60 * 1000)), // Last 7 days
  Q.where('is_pinned', Q.notEq(true)), // Not pinned
  Q.sortBy('created_at', Q.desc),
  Q.take(10) // Limit to 10 results
).fetch()

// Text search (case-insensitive)
const searchPosts = await database.get('posts').query(
  Q.where('title', Q.like('%react native%')),
  Q.or(
    Q.where('title', Q.like('%watermelon%')),
    Q.where('body', Q.like('%database%'))
  )
).fetch()

// Join queries - get posts with their comment count
const postsWithCommentCount = await database.get('posts').query(
  Q.experimentalJoinTables(['comments']),
  Q.on('comments', 'post_id', 'posts.id'),
  Q.sortBy('created_at', Q.desc)
).fetch()

// Pagination
async function getPostsPaginated(page = 0, limit = 20) {
  const posts = await database.get('posts').query(
    Q.sortBy('created_at', Q.desc),
    Q.skip(page * limit),
    Q.take(limit)
  ).fetch()
  
  return posts
}

Real-time Queries with Observables

// Custom hook for live search
import { useState, useEffect } from 'react'
import { useDatabase } from '@nozbe/watermelondb/hooks'
import { Q } from '@nozbe/watermelondb'

function usePostSearch(searchTerm) {
  const [posts, setPosts] = useState([])
  const database = useDatabase()

  useEffect(() => {
    if (!searchTerm.trim()) {
      setPosts([])
      return
    }

    const query = database.get('posts').query(
      Q.where('title', Q.like(`%${searchTerm}%`)),
      Q.sortBy('created_at', Q.desc)
    )

    // Subscribe to live updates
    const subscription = query.observe().subscribe(setPosts)

    return () => subscription.unsubscribe()
  }, [searchTerm, database])

  return posts
}

// Usage in component
function SearchablePostList() {
  const [searchTerm, setSearchTerm] = useState('')
  const posts = usePostSearch(searchTerm)

  return (
    <View>
      <TextInput
        placeholder="Search posts..."
        value={searchTerm}
        onChangeText={setSearchTerm}
      />
      <FlatList
        data={posts}
        renderItem={({ item }) => <PostItem post={item} />}
        keyExtractor={item => item.id}
      />
    </View>
  )
}

8. Data Operations (CRUD)

Creating Records

// Always wrap writes in database.write()
await database.write(async () => {
  // Create a simple record
  const newPost = await database.get('posts').create(post => {
    post.title = 'My New Post'
    post.body = 'Post content here...'
    post.isPinned = false
  })

  // Create related records
  const comment = await database.get('comments').create(comment => {
    comment.body = 'Great post!'
    comment.authorName = 'John Doe'
    comment.post.set(newPost) // Set the relationship
  })

  return newPost
})

Updating Records

// Update a single record
await database.write(async () => {
  await post.update(post => {
    post.title = 'Updated Title'
    post.isPinned = true
  })
})

// Batch updates
await database.write(async () => {
  const updates = posts.map(post => 
    post.prepareUpdate(post => {
      post.isPinned = false
    })
  )
  
  await database.batch(...updates)
})

Deleting Records

// Delete a single record
await database.write(async () => {
  await post.destroyPermanently()
})

// Batch delete
await database.write(async () => {
  const deletions = oldPosts.map(post => post.prepareDestroyPermanently())
  await database.batch(...deletions)
})

// Cascade delete with relationships
await database.write(async () => {
  // This will also delete all related comments
  await post.destroyPermanently()
})

9. Performance Optimization

Batch Operations

For better performance, always batch multiple operations:

// ❌ Bad: Multiple separate writes
async function importPosts(postData) {
  for (const data of postData) {
    await database.write(async () => {
      await database.get('posts').create(post => {
        post.title = data.title
        post.body = data.body
      })
    })
  }
}

// βœ… Good: Single batched write
async function importPosts(postData) {
  await database.write(async () => {
    const posts = postData.map(data =>
      database.get('posts').prepareCreate(post => {
        post.title = data.title
        post.body = data.body
      })
    )
    
    await database.batch(...posts)
  })
}

Query Optimization

// Use indexes for frequently queried fields
const usersByEmail = await database.get('users').query(
  Q.where('email', email) // Fast if email is indexed
).fetch()

// Limit results when possible
const recentPosts = await database.get('posts').query(
  Q.sortBy('created_at', Q.desc),
  Q.take(20) // Only get what you need
).fetch()

// Use lazy loading for related data
const enhance = withObservables(['post'], ({ post }) => ({
  post,
  // Comments loaded only when component renders
  comments: post.comments.observe()
}))

Memory Management

// Use pagination for large datasets
function usePaginatedPosts(pageSize = 20) {
  const [posts, setPosts] = useState([])
  const [page, setPage] = useState(0)
  const [loading, setLoading] = useState(false)
  const database = useDatabase()

  const loadMore = async () => {
    if (loading) return
    
    setLoading(true)
    const newPosts = await database.get('posts').query(
      Q.sortBy('created_at', Q.desc),
      Q.skip(page * pageSize),
      Q.take(pageSize)
    ).fetch()
    
    setPosts(current => [...current, ...newPosts])
    setPage(current => current + 1)
    setLoading(false)
  }

  return { posts, loadMore, loading }
}

10. Testing WatermelonDB

Setting Up Tests

// __tests__/database.test.js
import { Database } from '@nozbe/watermelondb'
import LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
import { mySchema } from '../model/schema'
import Post from '../model/Post'
import Comment from '../model/Comment'

// Use LokiJS for faster in-memory testing
function createTestDatabase() {
  const adapter = new LokiJSAdapter({
    schema: mySchema,
    useWebWorker: false,
    useIncrementalIndexedDB: false,
  })

  return new Database({
    adapter,
    modelClasses: [Post, Comment],
  })
}

describe('Post Model', () => {
  let database

  beforeEach(() => {
    database = createTestDatabase()
  })

  it('should create a post', async () => {
    let post
    await database.write(async () => {
      post = await database.get('posts').create(p => {
        p.title = 'Test Post'
        p.body = 'Test content'
      })
    })

    expect(post.title).toBe('Test Post')
    expect(post.body).toBe('Test content')
  })

  it('should create comments for post', async () => {
    let post, comment
    await database.write(async () => {
      post = await database.get('posts').create(p => {
        p.title = 'Test Post'
        p.body = 'Test content'
      })

      comment = await database.get('comments').create(c => {
        c.body = 'Test comment'
        c.authorName = 'Test Author'
        c.post.set(post)
      })
    })

    const comments = await post.comments.fetch()
    expect(comments).toHaveLength(1)
    expect(comments[0].body).toBe('Test comment')
  })
})

11. Migration and Schema Updates

As your app evolves, you'll need to update your database schema. WatermelonDB provides a migration system to handle this safely.

Creating Migrations

// migrations.js
import { schemaMigrations, addColumns, createTable } from '@nozbe/watermelondb/Schema/migrations'

export default schemaMigrations({
  migrations: [
    // Migration from version 1 to 2
    {
      toVersion: 2,
      steps: [
        addColumns({
          table: 'posts',
          columns: [
            { name: 'featured_image_url', type: 'string', isOptional: true },
            { name: 'view_count', type: 'number' }
          ]
        })
      ]
    },
    // Migration from version 2 to 3
    {
      toVersion: 3,
      steps: [
        createTable({
          name: 'categories',
          columns: [
            { name: 'name', type: 'string' },
            { name: 'description', type: 'string', isOptional: true },
            { name: 'color', type: 'string' },
            { name: 'created_at', type: 'number' },
            { name: 'updated_at', type: 'number' }
          ]
        }),
        addColumns({
          table: 'posts',
          columns: [
            { name: 'category_id', type: 'string', isIndexed: true, isOptional: true }
          ]
        })
      ]
    }
  ]
})

Applying Migrations

// database/index.js
import migrations from './migrations'

const adapter = new SQLiteAdapter({
  schema: mySchema,
  migrations,
  onSetUpError: error => {
    console.error('Database setup error:', error)
  }
})

// Update your schema version
export const mySchema = appSchema({
  version: 3, // Increment when adding migrations
  tables: [
    // ... your tables
  ]
})

12. Best Practices and Common Pitfalls

Do's and Don'ts

βœ… Best Practices:
  • Always wrap writes in database.write()
  • Use batch operations for multiple changes
  • Index columns you frequently query by
  • Use withObservables for reactive components
  • Implement proper error handling
  • Use migrations for schema changes in production
  • Test your database operations
❌ Common Pitfalls:
  • Don't perform writes outside database.write()
  • Avoid multiple separate writes - use batching instead
  • Don't index every column - it hurts performance
  • Never modify schema version without migrations in production
  • Don't forget to handle loading states in your UI
  • Avoid deeply nested queries - optimize with proper indexing

Error Handling

// Proper error handling
async function createPostSafely(title, body) {
  try {
    await database.write(async () => {
      const post = await database.get('posts').create(post => {
        post.title = title
        post.body = body
      })
      return post
    })
  } catch (error) {
    console.error('Failed to create post:', error)
    // Handle error appropriately
    throw new Error('Failed to create post. Please try again.')
  }
}

// Handle database connection issues
const adapter = new SQLiteAdapter({
  schema: mySchema,
  onSetUpError: error => {
    console.error('Database setup failed:', error)
    // Could show user-friendly error message
    // or fallback to offline mode
  }
})

13. Real-World Example: Todo App

Let's build a complete example to see WatermelonDB in action:

Schema Definition

// model/schema.js
import { appSchema, tableSchema } from '@nozbe/watermelondb'

export const todoSchema = appSchema({
  version: 1,
  tables: [
    tableSchema({
      name: 'todos',
      columns: [
        { name: 'text', type: 'string' },
        { name: 'is_completed', type: 'boolean' },
        { name: 'priority', type: 'string' }, // 'low', 'medium', 'high'
        { name: 'due_date', type: 'number', isOptional: true },
        { name: 'created_at', type: 'number' },
        { name: 'updated_at', type: 'number' }
      ]
    })
  ]
})

Todo Model

// model/Todo.js
import { Model } from '@nozbe/watermelondb'
import { field, date } from '@nozbe/watermelondb/decorators'

export default class Todo extends Model {
  static table = 'todos'

  @field('text') text
  @field('is_completed') isCompleted
  @field('priority') priority
  @date('due_date') dueDate
  @date('created_at') createdAt
  @date('updated_at') updatedAt

  get isOverdue() {
    if (!this.dueDate || this.isCompleted) return false
    return this.dueDate < Date.now()
  }

  async toggle() {
    await this.update(todo => {
      todo.isCompleted = !todo.isCompleted
    })
  }
}

Todo List Component

// components/TodoList.js
import React, { useState } from 'react'
import { View, Text, FlatList, TouchableOpacity, TextInput, Alert } from 'react-native'
import { withDatabase } from '@nozbe/watermelondb/DatabaseProvider'
import { withObservables } from '@nozbe/watermelondb/react'
import { Q } from '@nozbe/watermelondb'

function TodoList(props) {
  const { todos, database } = props
  const [newTodoText, setNewTodoText] = useState('')

  const addTodo = async () => {
    if (!newTodoText.trim()) return

    try {
      await database.write(async () => {
        await database.get('todos').create(todo => {
          todo.text = newTodoText.trim()
          todo.isCompleted = false
          todo.priority = 'medium'
        })
      })
      setNewTodoText('')
    } catch (error) {
      Alert.alert('Error', 'Failed to add todo')
    }
  }

  const deleteTodo = async (todo) => {
    try {
      await database.write(async () => {
        await todo.destroyPermanently()
      })
    } catch (error) {
      Alert.alert('Error', 'Failed to delete todo')
    }
  }

  const renderTodo = ({ item: todo }) => (
    <View style={{ 
      flexDirection: 'row', 
      padding: 15, 
      borderBottomWidth: 1,
      backgroundColor: todo.isCompleted ? '#f0f0f0' : 'white'
    }}>
      <TouchableOpacity 
        onPress={() => todo.toggle()}
        style={{ marginRight: 15 }}
      >
        <Text style={{ fontSize: 20 }}>
          {todo.isCompleted ? 'βœ…' : 'β­•'}
        </Text>
      </TouchableOpacity>
      
      <View style={{ flex: 1 }}>
        <Text style={{ 
          textDecorationLine: todo.isCompleted ? 'line-through' : 'none',
          color: todo.isOverdue ? 'red' : 'black'
        }}>
          {todo.text}
        </Text>
        <Text style={{ color: 'gray', fontSize: 12 }}>
          Priority: {todo.priority}
          {todo.isOverdue && ' β€’ OVERDUE'}
        </Text>
      </View>
      
      <TouchableOpacity 
        onPress={() => deleteTodo(todo)}
        style={{ padding: 5 }}
      >
        <Text style={{ color: 'red' }}>Delete</Text>
      </TouchableOpacity>
    </View>
  )

  return (
    <View style={{ flex: 1 }}>
      <View style={{ padding: 15, borderBottomWidth: 1 }}>
        <TextInput
          placeholder="Add new todo..."
          value={newTodoText}
          onChangeText={setNewTodoText}
          onSubmitEditing={addTodo}
          style={{ borderWidth: 1, padding: 10, borderRadius: 5 }}
        />
      </View>
      
      <FlatList
        data={todos}
        renderItem={renderTodo}
        keyExtractor={item => item.id}
      />
    </View>
  )
}

const enhance = withObservables([], ({ database }) => ({
  todos: database.collections.get('todos').query(
    Q.sortBy('created_at', Q.desc)
  )
}))

export default withDatabase(enhance(TodoList))

Conclusion

WatermelonDB represents a paradigm shift in how we approach database management in React Native applications. By combining the reliability of SQLite with reactive programming principles and lazy loading, it solves the fundamental performance challenges that plague traditional database solutions in mobile apps.

🎯 Key Takeaways:
  • Performance First: WatermelonDB's lazy loading ensures your app launches instantly, regardless of data size
  • Reactive by Design: Automatic UI updates when data changes eliminate manual state management
  • Production Ready: Powers apps like Nozbe Teams with tens of thousands of users
  • Developer Experience: Clean APIs, TypeScript support, and excellent documentation
  • Scalable Architecture: Built to grow with your app from prototype to enterprise

The reactive nature of WatermelonDB isn't just a technical featureβ€”it's a fundamental shift that makes your components cleaner, your data flow more predictable, and your app more maintainable. When you combine this with the performance benefits of lazy loading and SQLite's reliability, you get a database solution that scales effortlessly from hundreds to tens of thousands of records.

πŸš€ Next Steps:
  1. Start with a simple schema and gradually add complexity
  2. Implement proper indexing strategies for your query patterns
  3. Use batch operations for better performance
  4. Set up comprehensive testing from the beginning
  5. Plan your migration strategy for production apps
  6. Explore sync capabilities for offline-first experiences

Whether you're building a note-taking app, a social platform, or a complex enterprise application, WatermelonDB provides the foundation you need to create fast, reliable, and maintainable React Native apps. The investment in learning WatermelonDB pays dividends as your app grows in complexity and user base.

Remember: the best database is one that gets out of your way and lets you focus on building great user experiences. WatermelonDB does exactly thatβ€”it handles the complex database operations efficiently while providing a clean, reactive interface that makes your React Native development more enjoyable and productive.

πŸ‰ Start Your WatermelonDB Journey Today!
Don't let database performance hold back your React Native app. Start with the examples in this guide, build your first WatermelonDB-powered feature, and experience the difference that a truly reactive, high-performance database can make.

πŸš€ Ready to catch UI issues before your users do?

Use Viewlytics to automatically capture and analyze your app's UI across real devices. Our AI-powered platform helps you identify visual bugs, layout issues, and inconsistencies before they impact user experience.

Start UI Testing with Viewlytics
logo

2025 Β© Viewlytics. All rights reserved.