reactive_forms icon indicating copy to clipboard operation
reactive_forms copied to clipboard

`ReactiveFormArray` doesn't rebuild when `formArray.status` changes due to AsyncValidator

Open Giuspepe opened this issue 3 years ago • 4 comments

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

Giuspepe avatar Feb 09 '23 10:02 Giuspepe

Hi @Giuspepe did you find any workaround for this issue ?

sagnik-sanyal avatar May 06 '24 17:05 sagnik-sanyal

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

Giuspepe avatar May 06 '24 18:05 Giuspepe

Thanks man for your quick response

sagnik-sanyal avatar May 06 '24 19:05 sagnik-sanyal

Upvoted this issue , current workaround is to use synchronous validators instead of async validator. @joanpablo Looking forward to hear from you regarding this.

sagnik-sanyal avatar May 18 '24 10:05 sagnik-sanyal