Firebasertdb live data sync with no grid refresh

Hey I would like to sync firebasertdb live data changes to syncfusion grid component and make sure that, when in the database adding new records, deleting records, updating cell values does not trigger a grid refresh and scroll back to the top. I have kinda managed to achieve cell update not to trigger refreshes as well as adding new records but deletion seems to still trigger a grid refresh. Maybe I am approaching it completely wrong, maybe you could guide me at most elegant way to achieve live data updates of added/deleted records as well as cell level updates without triggering refreshes:

<ejs-grid
      #grid
      [dataSource]="data | async"
      height="75vh"
      [allowResizing]="true"
      [allowSorting]="true"
      [allowFiltering]="true"
      [filterSettings]="filterSettings"
      [toolbar]="toolbarOptions"
      [pageSettings]="options"
      [enableImmutableMode]="true"
      (created)="created()"
      (dataBound)="onDataBound()"
      allowTextWrap="true"
      [allowPaging]="true"
      [editSettings]="editSettings"
      (actionComplete)='onActionComplete($event)'
    >
      <e-columns>
        <e-column headerText="Image" width="100" textAlign="Center">
          <ng-template #template let-data>
            <div class="image">
              <img [src]="data.images[0]?.replace('s-l1600.webp', 's-l100.webp')" />
            </div>
          </ng-template>
        </e-column>

        <e-column
          field="item_id"
          width="180"
          headerText="ID"
          [isPrimaryKey]="true"
        ></e-column>
        <e-column field="title" width="150" headerText="Title"></e-column>
        <e-column field="price" width="110" headerText="Price"></e-column>


 private database = inject(Database);
  dataSubject = new BehaviorSubject<any[]>([]);
  data = this.dataSubject.asObservable();
 @ViewChild('grid') public grid?: GridComponent;
  public filterSettings?: object;
  public toolbarOptions?: string[];
  public options?: PageSettingsModel;

  public totalCount = 0;
  public visibleCount = 0;
  public editSettings = {
    allowEditing: true,
    allowAdding: true,
    allowDeleting: true,
    mode: 'Dialog' // You can also use 'Dialog' or 'Batch'
  };
 ngOnInit(): void {
    this.toolbarOptions = ['Add','Edit','Delete', 'Search'];
    this.filterSettings = { type: 'Excel' };
    this.options = { pageSize: 25 };
  }
constructor(private http: HttpClient) {
    this.fetchData();
  }
highlightCell(pk: string, field: string) {
    const rowIndex = this.grid?.getRowIndexByPrimaryKey(pk);
    if (rowIndex === undefined || rowIndex < 0) return;

    const cell = this.grid?.getCellFromIndex(
      rowIndex,
      this.grid.getColumnIndexByField(field)
    );
    if (!cell) return;

    cell.classList.add('highlight');
    setTimeout(() => cell.classList.remove('highlight'), 4000); // Remove after 1s
  }
  fetchData() {
    const dbRef = ref(this.database, 'accounts/blazinskasjonas@gmail%2Ecom');

    onValue(dbRef, (snapshot) => {
      if (!snapshot.exists()) {
        this.dataArray.length = 0;
        this.dataSubject.next([]);
        return;
      }

      const rawData = snapshot.val();
      const newData = Object.entries(rawData).map(([key, value]: any) => ({
        firebaseKey: key,
        ...value,
      }));

      const newIds = new Set(newData.map((item) => item.item_id));
      const existingIds = new Set(this.dataArray.map((item) => item.item_id));

      // 🔻 Handle removed rows
      const removedIds = [...existingIds].filter((id) => !newIds.has(id));
      for (const removedId of removedIds) {
        console.log(removedId)
        const index = this.dataArray.findIndex(
          (item) => item.item_id === removedId
        );
        if (index !== -1) {
          this.dataArray.splice(index, 1);
       
          const rowIndex = this.grid?.getRowIndexByPrimaryKey(removedId);
          if (rowIndex !== -1 && rowIndex != null) {
            // Delete directly from grid, avoids full refresh
            this.grid?.deleteRecord('item_id', { item_id: removedId });
          } else {
            // Update observable only if row was not visible
            this.dataSubject.next([...this.dataArray]);
          }
        }
      }

      // 🔁 Update or add rows
      for (const newItem of newData) {
        const pk = newItem.item_id;
        const index = this.dataArray.findIndex((item) => item.item_id === pk);

        if (index === -1) {
          // New row
          this.dataArray.push(newItem);
          this.dataSubject.next([...this.dataArray]);
        } else {
          const existingItem = this.dataArray[index];
          const keys = new Set([
            ...Object.keys(existingItem),
            ...Object.keys(newItem),
          ]);

          for (const key of keys) {
            const oldValue = existingItem[key];
            const newValue = newItem.hasOwnProperty(key)
              ? newItem[key]
              : undefined;

            if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
              // Update or blank out removed property
              this.grid?.setCellValue(pk, key, newValue ?? '');
              this.highlightCell(pk, key);
              if (newValue === undefined) {
                delete existingItem[key]; // cleanup
              } else {
                existingItem[key] = newValue;
              }
            }
          }
        }
      }

      this.totalCount = this.dataArray.length;
      this.visibleCount = this.grid?.getCurrentViewRecords().length || 0;
    });
  }


5 Replies

JC Joseph Christ Nithin Issack Syncfusion Team May 7, 2025 11:15 AM UTC


Hi Jonas,


   Greetings from Syncfusion support.


    Based on your query, we understand that you are trying to perform CRUD operation in the grid using the observable data. You have achieved to modify the cell details and addition of the records without refreshing the grid and maintaining the state of the scrollbar. However, while deleting the records, you are facing a grid refresh which makes the scroller to move to the top.


    On inspecting the code example provided we can understand that you are performing all the CRUD operations manually and updating the datasource of the grid directly. We suggest you use the custom data binding. By default, in EJ2 Grid, while using custom data binding, when you perform grid actions like filtering, sorting etc. the ‘dataStateChange’ event is triggered and when you perform CRUD operations the  ‘dataSourceChange’ event will be triggered. We have discussed the same in the below documentation for more details, please refer the below documentation.


Documentation: https://ej2.syncfusion.com/angular/documentation/grid/data-binding/remote-data#handling-crud-operations


  For Each CRUD operations performed the grid the ‘dataSourceChanged’ event will be triggered. Using this you can get all changes in dataSourceChanged event’s argument when args.requestType as batchsave if you are using the batch editing. Using this changes you can call your service and perform your action as per your requirement.


Please refer the below code example.


 

app.component.ts

 

export class NormalEditComponent implements OnInit {

     public data: Observable<DataStateChangeEventArgs>;

    public pageOptions: Object;

    public editSettings: Object;

    public toolbar: string[];

    public state: DataStateChangeEventArgs;

    customers: Customer[];

    @ViewChild('grid')

    public grid: GridComponent;

    constructor(private crudService: CrudService) {

        this.data = crudService;

    }

 

    getData(): void {

        this.crudService.getAllData()

            .subscribe((customers) => {

                alert(JSON.stringify(customers));

            });

    }

 

    public dataStateChange(state: DataStateChangeEventArgs): void {

        this.crudService.execute(state);

    }

 

    public dataSourceChanged(state: DataSourceChangedEventArgs): void {

        if (state.action === 'add') {

            this.crudService.addRecord(state).subscribe(() => { this.grid.notify('recordAdded', state); state.endEdit() });

        } else if (state.action === 'edit') {

            this.crudService.updateRecord(state).subscribe(() => state.endEdit());

        } else if (state.requestType === 'delete') {

            this.crudService.deleteRecord(state).subscribe(() => {

                state.endEdit();

            });

        }

    }

 

    public ngOnInit(): void {

        this.editSettings = { allowEditing: true, allowAdding: true, allowDeleting: true, mode: 'Dialog' };

        this.toolbar = ['Add', 'Edit', 'Delete', 'Update', 'Cancel'];

        const state: any = { skip: 0, take: 12 };

        this.crudService.execute(state);

    }

}

 


The below screenshot shows the arguments that are passed to the server when an edit operation is performed. Now the dataSourceChanged event is triggered and we can see the changes done in the dataSourceChanged event’s arguments that are highlighted in the below screenshot.


 



We have also attached a sample to perform CRUD operations.


Sample: https://stackblitz.com/edit/angular-async-crud-320133-nzgvjld7?file=normal-edit.html


Regards,

Joseph I.




JB Jonas Blazinskas May 7, 2025 12:05 PM UTC

Hi, I believe I implemented your method just the way you laid it out with some tweaks for undefined catches. But the result seems to be breaking pagination. 
All of the data gets loaded in completely ignoring pagination, so on the first page, I can see and scroll all of the 900 items instead of 25 as my pagination should be and was with my old method, which seems to be really weird. Please let me know what you think is going on






JB Jonas Blazinskas May 7, 2025 12:07 PM UTC

Image attached as it would not let me share screenshot 


Attachment: Screenshot_20250507_130639_34dad78b.png


JB Jonas Blazinskas May 7, 2025 02:03 PM UTC

another problem seems to be that when data changes on the firebase server(not using the grids add/edit/delete buttons aka for example another client) the data is not updated in the grid, after searching it is 



DM Dineshnarasimman Muthu Syncfusion Team May 14, 2025 04:37 AM UTC

Hi,


We have reviewed your query, it seems when implementing the data binding using custom data binding approach, it seems that all the records are rendered in the same page. We would like to tell you that when binding the data in result and count format, for every grid action(such as Sort, Filter, Page, etc.,), we have triggered the dataStateChange event and, in that event arguments we have send the corresponding action details(like skip, take, filter field, value, sort direction, etc.,) based on that, you can perform the action in your service and return the data as a result and count object.


For initial rendering:

   Here for initial rendering the skip and take values should be set manually and sent to the server in the ngOnInit. Also set the sort and group settings data, to persist the state of the grid in the initial rendering of the grid.

 



For Paging:


 Here when the paging is done the `dataStateChange` event in triggered. In this event’s arguments, the details required for getting the desired records from the server is passed in the argument to the service.




We have attached the sample for your reference.


Sample: https://stackblitz.com/edit/angular-grid-u7mdtf-8nsffxbb


Query: Grid data not updating when the database is updated:


To keep your Syncfusion EJ2 Grid in sync with real-time updates from Firebase (or any other real-time database), the grid must be manually updated on the client side when changes occur. Syncfusion EJ2 Grid does not automatically detect external database changes.


With Firebase, you can listen for data changes (e.g., using onValue, onChildChanged, etc. in the Realtime Database or Firestore listeners). When these events are triggered, you can then update the EJ2 Grid's data manually.


We also noticed that in the onValue, you get the updated data and using setCellValue method to update the data, we would like to tell you that setCellValue only updates the data in the UI level. We suggest you to use updateRow method and pass the rowData as the parameter.


The API documentation has been attached for your reference.


Documentation: https://ej2.syncfusion.com/angular/documentation/api/grid/#updaterow


Please get back to us for further assistance.


Regards,

Dineshnarasimman M



Loader.
Up arrow icon