Skip to main content

Data Flow Overview

Data flows through the application in a unidirectional pattern, ensuring predictable behavior and making the system easier to reason about.

Flow Diagram

Data Flow Patterns

Create Operation

1

User Input

User fills a form and submits in the UI component
2

Hook Call

UI component calls the create() method from useRecords hook
3

Service Processing

Service generates UUID, timestamps, and creates Record entity
4

Repository Persistence

Repository executes SQL INSERT statement
5

Database Storage

SQLite stores the record
6

State Refresh

Hook reloads data and updates component state
7

UI Update

Component re-renders with updated data

Read Operation

1

Component Mount

Component mounts and hook’s useEffect runs
2

Load Data

Hook calls load() which invokes service.list()
3

Repository Query

Repository executes SELECT * FROM records WHERE isDeleted = 0
4

Data Mapping

Repository maps database rows to Record entities
5

State Update

Hook updates local state with records
6

Render

Component renders the list of records

Update Operation

1

Edit Action

User modifies a record and saves changes
2

Hook Update

UI calls update(record) from hook
3

Timestamp Update

Service updates the updatedAt timestamp
4

Repository Update

Repository executes SQL UPDATE statement
5

Refresh

Hook reloads all records to reflect changes
6

Re-render

UI updates with modified data

Delete Operation (Soft Delete)

1

Delete Action

User confirms deletion
2

Hook Call

UI calls remove(id) from hook
3

Service Delete

Service calls repository’s softDelete(id)
4

Flag Update

Repository sets isDeleted = 1 via UPDATE query
5

Refresh

Hook reloads records (deleted ones are filtered out)
6

UI Update

Component re-renders without the deleted record

Complete Example: Creating a Record

Let’s trace a complete data flow for creating a new record:

1. UI Component (Presentation Layer)

import { useRecords } from '@/src/presentation/hooks/useRecords';

export function RecordsScreen() {
  const { records, create } = useRecords();
  const [title, setTitle] = useState('');

  const handleCreate = async () => {
    try {
      // Step 1: Call hook's create method
      await create(title, 'client');
      setTitle(''); // Clear input
    } catch (error) {
      alert(error.message);
    }
  };

  return (
    <View>
      <TextInput value={title} onChangeText={setTitle} />
      <Button title="Create" onPress={handleCreate} />
      {records.map(record => <RecordCard key={record.id} record={record} />)}
    </View>
  );
}

2. Custom Hook (Presentation Layer)

import { RecordService } from '@/src/application/services/RecordService';

export function useRecords() {
  const service = useMemo(() => new RecordService(), []);
  const [records, setRecords] = useState<Record[]>([]);

  const load = async () => {
    // Step 6: Reload data after creation
    const data = await service.list();
    setRecords(data); // Step 7: Update state
  };

  const create = async (title: string, type: string) => {
    // Step 2: Forward to service
    await service.create(title, type);
    await load(); // Step 5: Refresh after creation
  };

  return { records, create, load };
}

3. Service (Application Layer)

import { RecordRepository } from '@/src/infraestructure/repositories/RecordRepository';
import * as Crypto from 'expo-crypto';

export class RecordService {
  private repository = new RecordRepository();

  async create(title: string, type: string) {
    // Step 3: Business logic - generate ID and timestamps
    const now = new Date().toISOString();
    
    const record: Record = {
      id: Crypto.randomUUID(),
      title,
      type,
      createdAt: now,
      updatedAt: now,
      isDeleted: false,
    };

    // Step 4: Delegate to repository
    await this.repository.create(record);
    
    return record;
  }
}

4. Repository (Infrastructure Layer)

import { db } from '../database/database';

export class RecordRepository {
  async create(record: Record) {
    // Step 4a: Execute SQL INSERT
    await db.runAsync(
      `INSERT INTO records (
        id, title, subtitle, metadata, type, userId,
        createdAt, updatedAt, isDeleted
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      [
        record.id,
        record.title,
        record.subtitle ?? null,
        record.metadata ?? null,
        record.type,
        record.userId ?? null,
        record.createdAt,
        record.updatedAt,
        record.isDeleted ? 1 : 0,
      ]
    );
  }
}

5. Database (Infrastructure Layer)

import * as SQLite from "expo-sqlite";

// Step 4b: SQLite executes the INSERT
export const db = SQLite.openDatabaseSync("simple_manager.db");

Error Handling Flow

Errors propagate upward through the layers:
// 1. Database error occurs
db.runAsync(...) // Throws: "UNIQUE constraint failed"

// 2. Repository lets it bubble up
async create(record: Record) {
  await db.runAsync(...); // Error propagates
}

// 3. Service might transform it
async create(title: string, type: string) {
  try {
    await this.repository.create(record);
  } catch (error) {
    // Could log or transform error here
    throw error;
  }
}

// 4. Hook catches and handles
const create = async (title: string, type: string) => {
  try {
    await service.create(title, type);
    await load();
  } catch (error) {
    throw new Error(getErrorMessage(error));
  }
};

// 5. UI displays to user
const handleCreate = async () => {
  try {
    await create(title, 'client');
  } catch (error) {
    alert(error.message); // User sees friendly message
  }
};

State Management

Local Component State

  • Form inputs
  • Loading states
  • Modal visibility
  • Temporary UI state

Hook State

  • Fetched data (records list)
  • Loading indicators
  • Error states

Global State (Zustand)

  • User session
  • App-wide settings
  • Theme preferences

Database State (Source of Truth)

  • Persisted records
  • User data
  • Application data
The database is always the single source of truth. UI state is ephemeral and rebuilt from database on app restart.

Data Refresh Strategy

The application uses a simple refresh strategy:
  1. After any mutation (create, update, delete)
  2. Reload all data from the database
  3. Update component state
  4. Trigger re-render
const create = async (title: string, type: string) => {
  await service.create(title, type);
  await load(); // Always refresh after mutation
};

const update = async (record: Record) => {
  await service.update(record);
  await load(); // Always refresh after mutation
};

const remove = async (id: string) => {
  await service.delete(id);
  await load(); // Always refresh after mutation
};
This strategy is simple and reliable. For larger datasets, consider implementing optimistic updates or pagination.

Future: API Integration

When migrating to an API backend, the data flow remains similar:
Only the Infrastructure Layer needs to change. Services, hooks, and UI remain unchanged.

Clean Architecture

Understand the architectural layers

Folder Structure

See where each piece lives

Build docs developers (and LLMs) love