Skip to content

mutationProvider

The mutationProvider manages server-side operations like creating, updating, or deleting data. Unlike queries, mutations are triggered imperatively and their state is exposed for UI feedback.

To use a mutation, you define the provider and then use the notifier to trigger the transition.

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fasq_riverpod/fasq_riverpod.dart';
// 1. Define the mutation provider <Data, Variables>
final createUserProvider = mutationProvider<User, String>(
(name) => api.createUser(name),
);
class CreateUserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 2. Watch the state for UI feedback
final mutation = ref.watch(createUserProvider);
return Column(
children: [
ElevatedButton(
onPressed: mutation.isLoading
? null
: () {
// 3. Trigger the mutation via the NOTIFIER
ref.read(createUserProvider.notifier).mutate('John Doe');
},
child: mutation.isLoading
? CircularProgressIndicator()
: Text('Create User'),
),
if (mutation.hasError)
Text('Error: ${mutation.error}'),
if (mutation.isSuccess)
Text('Created: ${mutation.data!.name}'),
],
);
}
}

[!IMPORTANT] Always call .mutate() on the notifier (ref.read(provider.notifier).mutate(...)) rather than attempting to call it on the state object.

The mutationProvider returns a MutationState<T> object, which provides the following properties:

  • status: The current MutationStatus (idle, loading, success, error).
  • data: The result of the mutation if successful.
  • error: The error object if the mutation failed.
  • isLoading, isSuccess, isError, isIdle: Helper getters for status.
  • hasData, hasError: Helper getters for presence of data/error.

Handle side effects like navigation, showing snackbars, or invalidating queries using MutationOptions:

final deleteUserProvider = mutationProvider<void, String>(
(userId) => api.deleteUser(userId),
options: MutationOptions(
onSuccess: (data, variables) {
// Invalidate related queries to refresh data
ref.read(fasqClientProvider).invalidateQuery('users'.toQueryKey());
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('User deleted')),
);
},
onError: (error, variables) {
print('Failed to delete: $error');
},
),
);

Implement optimistic updates to make your UI feel instant:

final updatePostProvider = mutationProvider<Post, Post>(
(post) => api.updatePost(post),
options: MutationOptions(
onMutate: (post) {
// 1. Cancel outgoing refetches
ref.read(fasqClientProvider).cancelQuery(QueryKeys.post(post.id));
// 2. Snapshot the current value
final previousPost = ref.read(fasqClientProvider).getQueryData<Post>(
QueryKeys.post(post.id)
);
// 3. Optimistically update to the new value
ref.read(fasqClientProvider).setQueryData(QueryKeys.post(post.id), post);
return previousPost; // Return for rollback
},
onError: (error, post, previousPost) {
// 4. Rollback on error
if (previousPost != null) {
ref.read(fasqClientProvider).setQueryData(
QueryKeys.post(post.id),
previousPost
);
}
},
onSettled: (data, error, post) {
// 5. Always refetch after error or success to synchronize
ref.read(fasqClientProvider).invalidateQuery(QueryKeys.post(post.id));
},
),
);

Fasq supports queueing mutations when the device is offline:

final createCommentProvider = mutationProvider<Comment, String>(
(content) => api.addComment(content),
options: MutationOptions(
queueWhenOffline: true, // Mutation will execute when back online
),
);