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.
Optimistic Update Flow
Section titled “Optimistic Update Flow”- Immediate UI Update - Cache handler updates UI instantly
- Server Request - Mutation sent to server
- Success Handling - Replace optimistic data with server response
- Automatic Rollback - Ferry automatically rolls back optimistic changes if mutation response has errors
Upsert Request with Cache Handler
Section titled “Upsert Request with Cache Handler”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!; }}Optimistic Cache Handler
Section titled “Optimistic Cache Handler”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(), ); }}Automatic Error Handling
Section titled “Automatic Error Handling”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.hasErrorsor null data - Cache Restoration - Automatically restores previous cache state
- Optimistic Response Cleanup - Removes optimistic data on failure
Key Principles
Section titled “Key Principles”- ID Importance - Always ensure proper ID handling for cache updates
- Immediate Feedback - Update UI instantly for better UX
- Automatic Rollback - Ferry handles error rollback automatically
- Type Conversion - Convert between mutation and query types properly
- State Management - Track optimistic update states in services
- Error Handling - Focus on logging and user feedback, not manual rollback
Next Steps
Section titled “Next Steps”- Learn cache handling strategies
- Understand subscription context usage
- Implement repository data cleaning