`ReactiveFormArray` doesn't rebuild when `formArray.status` changes due to AsyncValidator
When using a ReactiveFormArray with asyncValidators in its FormArray, the builder doesn't update when the FormArray.status changes from pending to valid due to the asyncValidators. Instead, it is stuck in pending even though the asyncValidators return immediately.
If I remove the asyncValidators and only use synchronous validators, it works as expected.
See the attached screen recording and minimum reproducible example.
When using a ReactiveFormArray with a FormArray which has asyncValidators, I expect ReactiveFormArray.builder to be rebuilt when the FormArray.status changes due to the asyncValidators.
https://user-images.githubusercontent.com/39117631/217793024-093049d2-675e-4ec1-849a-3fff9504017d.mp4
You can see that the FormArray.status does indeed change back to valid if I access FormArray.status in a StreamBuilder listening to FormArray.statusChanged. It's just the ReactiveFormArray.builder that doesn't receive the new FormArray.status. Compare the formArray.status Texts in the video in red (FormArray.status in StreamBuilder) and black (FormArray.status in ReactiveFormArray.builder). The black text stays at ControlStatus.pending when asyncValidators are enabled while it should be in sync with the red text.
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:reactive_forms/reactive_forms.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const ProviderScope(
child: MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
),
);
}
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final formArray = ref.read(myControllerProvider.notifier).formArray;
return Scaffold(
appBar: AppBar(
title: const Text('Demo'),
),
body: Center(
child: ReactiveFormArray(
formArray: formArray,
builder: (context, formArray, _) => SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () => formArray.asyncValidators.isEmpty
? ref
.read(myControllerProvider.notifier)
.setAsyncValidators()
: ref
.read(myControllerProvider.notifier)
.clearAsyncValidators(),
child: Text(
formArray.asyncValidators.isEmpty
? 'Enable Async Validators'
: 'Disable Async Validators',
),
),
Text(
'formArray.asyncValidators.isEmpty: ${formArray.asyncValidators.isEmpty}\n'
'formArray.pending: ${formArray.pending}\n'
'formArray.errors: ${formArray.errors}\n'
'formArray.status: ${formArray.status}',
style: TextStyle(fontWeight: FontWeight.bold),
),
StreamBuilder(
stream: formArray.statusChanged,
builder: (context, data) => Text(
'Inside formArray.statusChanged StreamBuilder:\n'
' - formArray.status: ${formArray.status}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
),
for (final control in formArray.controls)
Text(control.value.toString()),
],
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: ref.read(myControllerProvider.notifier).update,
tooltip: 'Update',
child: const Icon(Icons.update),
),
);
}
}
final myControllerProvider =
StateNotifierProvider<MyController, List<DateTime>>((ref) {
final controller = MyController();
ref.listenSelf((previous, next) {
// without clear(), the old formArray.controls would remain when the `next` list is shorter
controller.formArray.clear();
controller.formArray.value = [...next];
// trigger validation when setting values programatically
controller.formArray.markAllAsTouched();
});
return controller;
});
class MyController extends StateNotifier<List<DateTime>> {
MyController() : super([]);
void update() => state = [...state, DateTime.now()];
void setAsyncValidators() {
formArray.setAsyncValidators(_asyncValidators, autoValidate: true);
formArray.markAllAsTouched();
}
void clearAsyncValidators() {
formArray.clearAsyncValidators();
// formArray.clearAsyncValidators states:
// When you add or remove a validator at run time, you must call
// **updateValueAndValidity()** for the new validation to take effect.
formArray.updateValueAndValidity();
formArray.markAllAsTouched();
}
late final formArray = FormArray<DateTime>(
[],
validators: _validators,
asyncValidators: _asyncValidators,
);
final _validators = [Validators.minLength(4)];
final _asyncValidators = [_asyncValidator];
}
Future<Map<String, dynamic>?> _asyncValidator(
AbstractControl<dynamic> control) async {
return null;
}
pubspec.yaml
name: reactive_forms_example
environment:
sdk: '>=2.19.0 <3.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
reactive_forms: ^14.2.0
flutter_riverpod: ^2.1.3
Hi @Giuspepe did you find any workaround for this issue ?
Hi @Giuspepe did you find any workaround for this issue ?
Sorry @sagnik-sanyal, I don't remember looking into this any further as it was a year ago
Thanks man for your quick response
Upvoted this issue , current workaround is to use synchronous validators instead of async validator. @joanpablo Looking forward to hear from you regarding this.