> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ditto.live/llms.txt
> Use this file to discover all available pages before exploring further.

# Reacting to Data Changes

> This article provides an overview and how-to instructions for observing data changes within Ditto.

You can react to changes in your local store by setting up a store observer.
You can read more about store observers in [Accessing Data](/key-concepts/accessing-data#observing-changes).

Store observers are useful when you want to monitor changes from your local Ditto store and react to them immediately. For instance, when your end user updates their profile, asynchronously display changes in realtime.

## Setting Up Store Observers

Using the `registerObserver` method, set up an observer within the `store` namespace enclosed with a query that specifies the collection to watch for changes, as well as your logic to handle the incoming changes.

### Store Observer with Query Arguments

To associate arguments with your query add them as a parameter.

<CodeGroup>
  ```swift Swift theme={null}

  let observer = ditto.store.registerObserver(
    query: "SELECT * FROM cars WHERE color = :color",
    arguments: [ "color": "blue" ]){ result in /* handle change */ };
  ```

  ```kotlin Kotlin theme={null}
  ditto.store.registerObserver(
    "SELECT * FROM cars WHERE color = :color",
    mapOf("color" to "blue")) { queryResult ->
    queryResult.use { result ->
      /* handle change */
    }
  };
  ```

  ```javascript JS theme={null}
  const changeHandler = (result) => {
    // handle change
  }
  const observer = ditto.store.registerObserver(
    "SELECT * FROM cars WHERE color = :color",
    changeHandler,
    { color: 'blue' });
  ```

  ```typescript TypeScript theme={null}
  import { QueryResult, StoreObserver } from '@dittolive/ditto';

  const changeHandler = (result: QueryResult): void => {
    // handle change
  };

  const observer: StoreObserver = ditto.store.registerObserver(
    "SELECT * FROM cars WHERE color = :color",
    changeHandler,
    { color: 'blue' });
  ```

  ```java Java theme={null}
  import com.ditto.java.DittoStoreObserver;
  import static com.ditto.java.serialization.DittoCborSerializable.buildDictionary;

  // DittoQueryResult passed to your lambda expression will be auto-closed after
  // the lambda is invoked. You MUST NOT save the result or it's associated
  // DittoQueryResultItem objects as they will be invalid outside the lambda's scope.
  DittoStoreObserver observer = ditto.getStore().registerObserver(
      "SELECT * FROM cars WHERE color = :color",
      buildDictionary().put("color", "blue").build(),
      (result) -> {
          // handle change
      }
  );
  ```

  ```csharp C# theme={null}
  // Without Arguments
  var result = await ditto.Store.RegisterObserver(
    "SELECT * FROM cars",
    (result) => {
      using (result)
      {
        // handle change
      }
    });

  // With Arguments
  var result = ditto.Store.RegisterObserver(
    "SELECT * FROM cars WHERE color = :color",
    new Dictionary<string, object> { "color", "blue" },
    (result) => {
      // handle change
    });
  ```

  ```cpp C++ theme={null}
  auto observer = ditto.get_store().register_observer(
    "SELECT * FROM cars",
    {{"color", "blue"}}),
    [&](QueryResult result) { /* handle change */ });
  ```

  ```rust Rust theme={null}
  #[derive(Serialize)]
  struct Args {
      color: String,
  }

  //...

  let args = Args {
      color: "blue".to_string(),
  }

  let observer = ditto.store().register_observer(
    (
      "SELECT * from cars WHERE color = :color",
      args,
    ),
    move |result: QueryResult| {
      // handle change
    })?;
  ```

  ```dart Dart theme={null}
  final observer = ditto.store.registerObserver(
    "SELECT * FROM cars WHERE color = :color",
    arguments: { color: "blue" },
    onChange: (queryResult) {
      // handle change
    },
  );
  ```

  ```go Go theme={null}
  observer, err := dit.Store().RegisterObserver(
      "SELECT * FROM cars WHERE color = :color",
      ditto.QueryArguments{"color" : "blue"},
      func(result *ditto.QueryResult) {
          defer result.Close()  // cleanup

          // handle change
      }
  )
  if err != nil {
      return err
  }
  ```
</CodeGroup>

## Canceling a Store Observer

To cancel a store observer, call `cancel` on the observer object.

Once canceled, the store observer will stop processing in the background and will no longer call the provided callback.

<CodeGroup>
  ```swift Swift theme={null}

  observer.cancel()
  ```

  ```kotlin Kotlin theme={null}
  observer.close()
  ```

  ```javascript JS theme={null}
  observer.cancel()
  ```

  ```typescript TypeScript theme={null}
  observer.cancel();
  ```

  ```java Java theme={null}
  observer.close();
  ```

  ```csharp C# theme={null}
  observer.Cancel()
  ```

  ```cpp C++ theme={null}
  observer.cancel();
  ```

  ```rust Rust theme={null}
  observer.cancel()
  ```

  ```dart Dart theme={null}
  observer.cancel();
  ```

  ```go Go theme={null}
  observer.Cancel()
  ```
</CodeGroup>

## Accessing Store Observers

To access store observers from the local Ditto store:

<CodeGroup>
  ```swift Swift theme={null}

  ditto.store.observers
  ```

  ```kotlin Kotlin theme={null}
  ditto.store.observers
  ```

  ```javascript JS theme={null}
  ditto.store.observers
  ```

  ```typescript TypeScript theme={null}
  ditto.store.observers
  ```

  ```java Java theme={null}
  ditto.getStore().getObservers();
  ```

  ```csharp C# theme={null}
  ditto.Store.Observers
  ```

  ```cpp C++ theme={null}
  ditto.get_store().observers()
  ```

  ```rust Rust theme={null}
  ditto.store().observers()
  ```

  ```go Go theme={null}
  ditto.Store().Observers()
  ```
</CodeGroup>

## Diffing Results

<Info>
  This feature is available in v4.11 and above.
</Info>

There are some use cases where it may be required to calculate the difference
between previous and current query results. For example, when you want to update
a UI component or external HTTP system with the latest data, you may want to
know which items have been added, removed, or changed since the last time you
received the query result.  The Ditto SDK provides a `DittoDiffer` class that
allows you to **opt-in to diffing only when necessary**, thereby avoiding
unnecessary performance and memory costs.

However, diffing is computationally expensive. It requires storing the previous query
results in memory, leading to increased RAM usage, which can be particularly
problematic for applications handling large datasets or frequent updates.
Instead of computing diffs synchronously with every store observer update, it is
recommended to **debounce** diffing, processing changes out-of-band at a cadence
that best suits your use case.

<Warning>
  **Critical Memory Management**: QueryResults and QueryResultItems should be treated like database cursors. Always extract the data you need immediately and then close/dematerialize them. Never store QueryResultItems directly between observer emissions as this will cause memory bloat and potential crashes.
</Warning>

A `DittoDiffer` is given query result items and compares them to the previous set of items it has received:

<CodeGroup>
  ```swift Swift theme={null}
  let differ = DittoDiffer()
  var previousDocumentIds: [String] = [] // Store only extracted IDs

  let observer = ditto.store.registerObserver(
    query: "SELECT * FROM cars") { queryResult in
      let diff = differ.diff(queryResult.items)

      // Extract current document IDs and dematerialize items
      let currentDocumentIds = queryResult.items.map { item in
          let id = item.value["_id"] as? String ?? "unknown"
          item.dematerialize() // Release memory after extracting data
          return id
      }

      // Handle deletions using stored IDs from previous emission
      for index in diff.deletions {
          let deletedId = previousDocumentIds[index]
          print("Deleted car with ID: \(deletedId)")
      }

      // Handle insertions using current IDs
      for index in diff.insertions {
          let insertedId = currentDocumentIds[index]
          print("Inserted car with ID: \(insertedId)")
      }

      // Handle updates using current IDs
      for index in diff.updates {
          let updatedId = currentDocumentIds[index]
          print("Updated car with ID: \(updatedId)")
      }

      // Store only the document IDs for next callback - no live references!
      previousDocumentIds = currentDocumentIds
  }
  ```

  ```kotlin Kotlin theme={null}
  val differ = DittoDiffer()
  var previousDocumentIds: List<String> = emptyList() // Store only extracted IDs

  val observer = ditto.store.registerObserver(
    "SELECT * FROM cars") { result ->
      result.use { queryResult -> // Auto-closes result when done
          val diff = differ.diff(queryResult.items)

          // Extract current document IDs and dematerialize items
          val currentDocumentIds = queryResult.items.map { item ->
              val id = item.value["_id"] as? String ?: "unknown"
              item.dematerialize() // Release memory after extracting data
              id
          }

          // Handle deletions using stored IDs from previous emission
          diff.deletions.forEach { index ->
              val deletedId = previousDocumentIds.getOrNull(index)
              if (deletedId != null) {
                  println("Deleted car with ID: $deletedId")
              }
          }

          // Handle insertions using current IDs
          diff.insertions.forEach { index ->
              val insertedId = currentDocumentIds.getOrNull(index)
              println("Inserted car with ID: $insertedId")
          }

          // Handle updates using current IDs
          diff.updates.forEach { index ->
              val updatedId = currentDocumentIds.getOrNull(index)
              println("Updated car with ID: $updatedId")
          }

          // Store only the document IDs for next callback - no live references!
          previousDocumentIds = currentDocumentIds
      }
  }
  ```

  ```javascript JS theme={null}
  const differ = new Differ();
  let previousDocumentIds = []; // Store only extracted IDs

  const changeHandler = (queryResult) => {
      const diff = differ.diff(queryResult.items);

      // Extract current document IDs and dematerialize items
      const currentDocumentIds = queryResult.items.map(item => {
          const id = item.value._id || 'unknown';
          item.dematerialize(); // Release memory after extracting data
          return id;
      });

      // Handle deletions using stored IDs from previous emission
      diff.deletions.forEach(index => {
          const deletedId = previousDocumentIds[index];
          console.log('Deleted car with ID:', deletedId);
      });

      // Handle insertions using current IDs
      diff.insertions.forEach(index => {
          const insertedId = currentDocumentIds[index];
          console.log('Inserted car with ID:', insertedId);
      });

      // Handle updates using current IDs
      diff.updates.forEach(index => {
          const updatedId = currentDocumentIds[index];
          console.log('Updated car with ID:', updatedId);
      });

      // Store only the document IDs for next callback - no live references!
      previousDocumentIds = currentDocumentIds;
  }

  const observer = ditto.store.registerObserver(
    "SELECT * FROM cars",
    changeHandler);
  ```

  ```java Java theme={null}
  DittoDiffer differ = new DittoDiffer();
  List<String> previousDocumentIds = new ArrayList<>(); // Store only extracted IDs

  DittoStoreObserver observer = ditto.getStore().registerObserver(
      "SELECT * FROM cars",
      result -> {
          try (result) { // Auto-closes result when done
              DittoDiff diff = differ.diff(result.items);

              // Extract current document IDs and dematerialize items
              List<String> currentDocumentIds = new ArrayList<>();
              for (DittoQueryResultItem item : result.items) {
                  String id = item.getValue().get("_id") != null ?
                              item.getValue().get("_id").toString() : "unknown";
                  currentDocumentIds.add(id);
                  item.dematerialize(); // Release memory after extracting data
              }

              // Handle deletions using stored IDs from previous emission
              for (int index : diff.getDeletions()) {
                  if (index < previousDocumentIds.size()) {
                      String deletedId = previousDocumentIds.get(index);
                      System.out.println("Deleted car with ID: " + deletedId);
                  }
              }

              // Handle insertions using current IDs
              for (int index : diff.getInsertions()) {
                  String insertedId = currentDocumentIds.get(index);
                  System.out.println("Inserted car with ID: " + insertedId);
              }

              // Handle updates using current IDs
              for (int index : diff.getUpdates()) {
                  String updatedId = currentDocumentIds.get(index);
                  System.out.println("Updated car with ID: " + updatedId);
              }

              // Store only the document IDs for next callback - no live references!
              previousDocumentIds = currentDocumentIds;
          } catch (Exception e) {
              // Handle errors
          }
      }
  );
  ```

  ```csharp C# theme={null}
  var differ = new DittoDiffer();
  var previousDocumentIds = new List<string>(); // Store only extracted IDs

  var observer = ditto.Store.RegisterObserver(
    "SELECT * FROM cars",
    (result) =>
    {
      using (result) // Disposes result when done
      {
        var diff = differ.Diff(result.Items);

        // Extract current document IDs and dematerialize items
        var currentDocumentIds = result.Items.Select(item =>
        {
            var id = item.Value.ContainsKey("_id") ?
                     item.Value["_id"].ToString() : "unknown";
            item.Dematerialize(); // Release memory after extracting data
            return id;
        }).ToList();

        // Handle deletions using stored IDs from previous emission
        foreach (var index in diff.Deletions)
        {
            if (index < previousDocumentIds.Count)
            {
                var deletedId = previousDocumentIds[index];
                Console.WriteLine($"Deleted car with ID: {deletedId}");
            }
        }

        // Handle insertions using current IDs
        foreach (var index in diff.Insertions)
        {
            var insertedId = currentDocumentIds[index];
            Console.WriteLine($"Inserted car with ID: {insertedId}");
        }

        // Handle updates using current IDs
        foreach (var index in diff.Updates)
        {
            var updatedId = currentDocumentIds[index];
            Console.WriteLine($"Updated car with ID: {updatedId}");
        }

        // Store only the document IDs for next callback - no live references!
        previousDocumentIds = currentDocumentIds;
      }
    });
  ```

  ```cpp C++ theme={null}
  DittoDiffer differ;
  std::vector<std::string> previousDocumentIds; // Store only extracted IDs

  auto observer = ditto.get_store().register_observer(
    "SELECT * FROM cars",
    [&](QueryResult result) {
        DittoDiff diff = differ.diff(result.items());

        // Extract current document IDs and dematerialize items
        std::vector<std::string> currentDocumentIds;
        for (auto& item : result.items()) {
            auto value = item.value();
            std::string id = value.contains("_id") ?
                            value["_id"].get<std::string>() : "unknown";
            currentDocumentIds.push_back(id);
            item.dematerialize(); // Release memory after extracting data
        }

        // Handle deletions using stored IDs from previous emission
        for (size_t index : diff.deletions()) {
            if (index < previousDocumentIds.size()) {
                std::string deletedId = previousDocumentIds[index];
                std::cout << "Deleted car with ID: " << deletedId << std::endl;
            }
        }

        // Handle insertions using current IDs
        for (size_t index : diff.insertions()) {
            std::string insertedId = currentDocumentIds[index];
            std::cout << "Inserted car with ID: " << insertedId << std::endl;
        }

        // Handle updates using current IDs
        for (size_t index : diff.updates()) {
            std::string updatedId = currentDocumentIds[index];
            std::cout << "Updated car with ID: " << updatedId << std::endl;
        }

        // Store only the document IDs for next callback - no live references!
        previousDocumentIds = currentDocumentIds;

        // C++ QueryResult uses RAII - destructor handles cleanup
    });
  ```

  ```rust Rust theme={null}
  let mut differ = DittoDiffer::new();
  let mut previous_document_ids: Vec<String> = Vec::new(); // Store only extracted IDs

  let observer = ditto.store().register_observer(
    "SELECT * FROM cars",
    move |result: QueryResult| {
        let diff = differ.diff(result.iter());

        // Extract current document IDs and dematerialize items
        let mut current_document_ids: Vec<String> = Vec::new();
        for item in result.iter() {
            let value = item.value();
            let id = value.get("_id")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown")
                .to_string();
            current_document_ids.push(id);
            item.dematerialize(); // Release memory after extracting data
        }

        // Handle deletions using stored IDs from previous emission
        for &index in &diff.deletions {
            if let Some(deleted_id) = previous_document_ids.get(index) {
                println!("Deleted car with ID: {}", deleted_id);
            }
        }

        // Handle insertions using current IDs
        for &index in &diff.insertions {
            let inserted_id = &current_document_ids[index];
            println!("Inserted car with ID: {}", inserted_id);
        }

        // Handle updates using current IDs
        for &index in &diff.updates {
            let updated_id = &current_document_ids[index];
            println!("Updated car with ID: {}", updated_id);
        }

        // Store only the document IDs for next callback - no live references!
        previous_document_ids = current_document_ids;

        // Rust QueryResult uses RAII - Drop trait handles cleanup
    });
  ```

  ```dart Dart theme={null}
  final differ = DittoDiffer();
  final previousDocumentIds = <String>[]; // Store only extracted IDs

  final observer = ditto.store.registerObserver(
    "SELECT * FROM cars",
    onChange: (queryResult) {
      final diff = differ.diff(queryResult.items);

      // Extract current document IDs
      // Note: Flutter SDK doesn't have explicit dematerialize
      final currentDocumentIds = queryResult.items.map((item) {
        final id = item.value['_id'] as String? ?? 'unknown';
        return id;
      }).toList();

      // Handle deletions using stored IDs from previous emission
      for (final index in diff.deletions) {
        if (index < previousDocumentIds.length) {
          final deletedId = previousDocumentIds[index];
          print('Deleted car with ID: $deletedId');
        }
      }

      // Handle insertions using current IDs
      for (final index in diff.insertions) {
        final insertedId = currentDocumentIds[index];
        print('Inserted car with ID: $insertedId');
      }

      // Handle updates using current IDs
      for (final index in diff.updates) {
        final updatedId = currentDocumentIds[index];
        print('Updated car with ID: $updatedId');
      }

      // Store only the document IDs for next callback - no live references!
      previousDocumentIds = currentDocumentIds;

      // Flutter handles cleanup automatically
    }
  );
  ```

  ```go Go theme={null}
  differ := ditto.NewDiffer()
  var previousDocumentIds []string // Store only extracted IDs

  observer, err := dit.Store().RegisterObserver(
      "SELECT * FROM cars", nil,
      func(result *ditto.QueryResult) {
          defer result.Close()  // cleanup

          diff := differ.DiffResult(result)

          // Extract current document IDs
          var currentDocumentIDs []string
          for _, item := range result.Items() {
              id := "unknown"
              if val, ok := item["_id"]; ok {
                  id = val
              }
              currrentDocumentIDs = append(currentDocumentIDs, id)

              // The result.Items() iterator automatically cleans up
              // each item after use.
          }

          // Handle deletions using stored IDs from previous emissions
          for _, index := range diff.Deletions {
              if index < len(previousDocumentIDs) {
                  deletedID := previousDocumentIDs[index]
                  fmt.Printf("Deleted car with ID: %s\n", deletedID)
              }
          }

          // Handle insertions using current IDs
          for _, index := range diff.Insertions {
              insertedID := currentDocumentIDs[index]
              fmt.Printf("Inserted car with ID: %s\n", insertedID)
          }

          // Handle updates using current IDs
          for _, index := range diff.Updates {
              updatedID := currentDocumentIDs[index]
              fmt.Printf("Updated car with ID: %s\n", updatedID)
          }

          // Store only the document IDs for next callback - no live references!
          previousDocumentIDs = currentDocumentIDs

          // The defer statement above takes care of cleanup
      }
  )
  if err != nil {
      return err
  }
  ```
</CodeGroup>

The `diff()` method returns a `DittoDiff` containing:

* `insertions`: Indexes of new items in the new array
* `deletions`: Indexes of items that were removed from the old array
* `updates`: Indexes of items in the new array that were present in the old array and whose value has changed
* `moves`: Pairs of indexes showing items that changed position

### Example: Apple UIKit

Use this diff to update a UIKit class, such as `UITableView`, `UICollectionView` :

```swift Swift theme={null}
let collectionView = /* assuming a UICollectionView exists */
let differ = DittoDiffer()
ditto.store.registerObserver("...") { queryResult in

    /* ... Update data source content based on query result ... */

    let diff = differ.diff(queryResult.items)
    tableView.performBatchUpdates({
        let insertions = diff.insertions.map { IndexPath(item: $0, section: 0) }
        let deletions = diff.deletions.map { IndexPath(item: $0, section: 0) }
        let reloads = diff.updates.map { IndexPath(item: $0, section: 0) }

        collectionView.insertItems(at: insertions)
        collectionView.deleteItems(at: deletions)
        collectionView.reloadItems(at: reloads)

        collectionView.moves.map { move in
            let (from, to) = move
            collectionView.move(at: from, to: to)
        }
    }, completion: nil)
}
```

### Key considerations

* **Keep references to arrays yourself**: The differ doesn't provide access to the old and new items themselves, so they need to be retained by the user if needed.

* **No comparison of document metadata:** The differ performs a deep comparison of the value of each query result item but doesn't take into account any metadata. Applying a series of changes to a document will not cause it to show up in `updated` unless those changes result in a different value from the initial state to the final state.

* **No async diffing:** Computing a diff on a set of many query result items or very large query result items might block the device depending on its hardware.

### Migrating from Live Query Events

If you're using the legacy live query API that automatically provides diff information through `event` parameters, you can migrate to the newer store observer pattern with the `DittoDiffer` class. The differ provides equivalent functionality but requires manual management of previous results.

For a complete migration guide, see [Replacing Live Query Events with Store Observers](/dql/replacing-live-queries).
