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