Skip to content

Implement Optimistic Updates

Problem

You need to implement optimistic updates that immediately show changes in the UI while the mutation is processing, with proper rollback on errors.

  1. Immediate UI Update - Cache handler updates UI instantly
  2. Server Request - Mutation sent to server
  3. Success Handling - Replace optimistic data with server response
  4. Automatic Rollback - Ferry automatically rolls back optimistic changes if mutation response has errors

Configure the upsert request to use a cache handler:

class HotelBookingsUpsertRequestStrategy {
@override
GMutateHotelBookingsSaveReq build(
UpsertRequestParams<GMutateHotelBookingsSaveVars> params,
) {
_request = GMutateHotelBookingsSaveReq(
(b) => b
..vars = params.vars.toBuilder()
..requestId = '${requestId}_${params.vars.hotelBookingsArgs?.id ?? 'new'}'
..updateCacheHandlerKey = HotelBookingsCacheHandlerStrategyKeys
.hotelBookingsUpsertCacheHandler.name,
);
return _request!;
}
}

Implement immediate cache updates with proper ID handling:

class HotelBookingsUpsertCacheHandlerStrategy
implements CacheHandlerStrategy<
GMutateHotelBookingsSaveData,
GMutateHotelBookingsSaveVars
> {
@override
UpdateCacheHandler build(RequestContext requestContext) {
return (proxy, response) {
if (response.hasErrors || response.data?.hotelbookingsSave == null) {
_handleError(proxy, requestContext, response);
return;
}
final updatedItem = response.data!.hotelbookingsSave!;
final isCreateOperation = _isCreateOperation(requestContext);
if (isCreateOperation) {
_handleOptimisticCreate(proxy, requestContext, updatedItem);
} else {
_handleOptimisticUpdate(proxy, requestContext, updatedItem);
}
};
}
void _handleOptimisticCreate(
CacheProxy proxy,
RequestContext requestContext,
GMutateHotelBookingsSaveData_hotelbookingsSave newItem,
) {
final listRequest = _getListRequest(requestContext);
if (listRequest == null) return;
final existingData = proxy.readQuery(listRequest);
if (existingData?.hotelbookings == null) return;
final existingItems = existingData!.hotelbookings!.hotelbookingsItems?.toList() ?? [];
final convertedItem = _convertMutationToQueryItem(newItem);
final updatedData = existingData.rebuild((b) => b
..hotelbookings.hotelbookingsItems.insert(0, convertedItem)
..hotelbookings.totalCount = (existingData.hotelbookings!.totalCount ?? 0) + 1
);
proxy.writeQuery(listRequest, updatedData);
}
void _handleOptimisticUpdate(
CacheProxy proxy,
RequestContext requestContext,
GMutateHotelBookingsSaveData_hotelbookingsSave updatedItem,
) {
final listRequest = _getListRequest(requestContext);
if (listRequest == null) return;
final existingData = proxy.readQuery(listRequest);
if (existingData?.hotelbookings == null) return;
final existingItems = existingData!.hotelbookings!.hotelbookingsItems?.toList() ?? [];
final updatedItems = existingItems.map((item) {
if (item?.id == updatedItem.id) {
return _convertMutationToQueryItem(updatedItem);
}
return item;
}).toList();
final updatedData = existingData.rebuild((b) => b
..hotelbookings.hotelbookingsItems.replace(updatedItems)
);
proxy.writeQuery(listRequest, updatedData);
}
bool _isCreateOperation(RequestContext requestContext) {
final mutationVars = requestContext
.getHolder<GMutateHotelBookingsSaveReq>(
HotelBookingsRequestStrategyKeys.hotelBookingsUpsert.name,
)?.request?.vars;
final targetId = mutationVars?.hotelBookingsArgs?.id;
return targetId == null || targetId.isEmpty;
}
GQueryHotelBookingsData_hotelbookings_hotelbookingsItems _convertMutationToQueryItem(
GMutateHotelBookingsSaveData_hotelbookingsSave mutationItem,
) {
return GQueryHotelBookingsData_hotelbookings_hotelbookingsItems.fromJson(
mutationItem.toJson(),
);
}
}

Ferry provides automatic rollback when mutations fail:

UpdateCacheHandler build(RequestContext requestContext) {
return (proxy, response) {
if (response.hasErrors || response.data?.hotelbookingsSave == null) {
// Ferry automatically rolls back optimistic changes
// No manual rollback needed
print('❌ Optimistic update failed: ${response.graphqlErrors}');
return;
}
final updatedItem = response.data!.hotelbookingsSave!;
final isCreateOperation = _isCreateOperation(requestContext);
if (isCreateOperation) {
_handleOptimisticCreate(proxy, requestContext, updatedItem);
} else {
_handleOptimisticUpdate(proxy, requestContext, updatedItem);
}
};
}

Key Points About Ferry’s Automatic Rollback

Section titled “Key Points About Ferry’s Automatic Rollback”
  • No Manual Rollback Required - Ferry handles rollback automatically
  • Error Detection - Checks response.hasErrors or null data
  • Cache Restoration - Automatically restores previous cache state
  • Optimistic Response Cleanup - Removes optimistic data on failure
  1. ID Importance - Always ensure proper ID handling for cache updates
  2. Immediate Feedback - Update UI instantly for better UX
  3. Automatic Rollback - Ferry handles error rollback automatically
  4. Type Conversion - Convert between mutation and query types properly
  5. State Management - Track optimistic update states in services
  6. Error Handling - Focus on logging and user feedback, not manual rollback