Creating a reactive TextField in Flutter

In this article, I will show you how you can very easily create a reactive TextField in Flutter.

Motivation

A few months ago, while working on a client project, I was building multiple pages in a row which all contained some kind of form, like a login-, registration- and personal details form.

I soon got annoyed with all sorts of things related to building these forms...

  1. Each page had to be Stateful (StatefulWidget).

  2. All the TextEditingControllers and FocusNodes had to be created manually.

  3. We had to think about disposing these TextEditingControllers and FocusNodes.

  4. The TextEditingControllers had to be 'connected' with our state management.

  5. Validation ended up in several places, depending on if the validation was client- or server-sided.

All of this resulted in a painful amount of boilerplate code.

And I was not having it...

The Slider widget

A bit later, I had to create a form for my Scavenger Hunt app pet project. This form had only one TextField, but it also contained a Slider.

I realized the Slider was way simpler to work with. Its most important properties are: value and onChanged.
With value you tell the Slider its current value, and with onChanged you receive the value the user changed the slider to. No hassle of dealing with controllers.

So I was thinking, what if I could make a TextField that works the same way?

A Reactive TextField was born

After playing around with some ideas, I ended up with something that can be used like this:

ReactiveTextField(
  text: state.counter.toString(),
  onChange: context.read<CounterCubit>().setValue,
  error: state.error,
),

As you can see, there are no controllers to deal with. Its properties can be directly connected with the state management solution of your choosing.

Below you can see its implementation.

class ReactiveTextField extends StatefulWidget {
  const ReactiveTextField({
    super.key,
    required this.text,
    required this.onChange,
    this.error,
  });

  final String text;
  final void Function(String text) onChange;
  final String? error;

  @override
  State<ReactiveTextField> createState() => _ReactiveTextFieldState();
}

class _ReactiveTextFieldState extends State<ReactiveTextField> {
  // ReactiveTextField will hold onto its own TextEditingController and Focusnode,
  // which you otherwise had to create and maintain yourself.
  final _controller = TextEditingController();
  final _focusNode = FocusNode();

  @override
  void initState() {
    super.initState();

    _controller.text = widget.text;
  }

  @override
  void didUpdateWidget(covariant ReactiveTextField oldWidget) {
    super.didUpdateWidget(oldWidget);
    // This is an important part!
    // Without this check, there will be an infinite loop!
    if (widget.text != _controller.text) {
      _controller.text = widget.text;
     // We remove focus, because the value has been set
     // from a different source.
      _focusNode.unfocus();
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    _focusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // ReactiveTextField simply wraps a 'normal' TextField.
    return TextField(
      controller: _controller,
      focusNode: _focusNode,
      onChanged: widget.onChange,
      decoration: InputDecoration(
        errorText: widget.error,
      ),
    );
  }
}

As you can see, it is very simple. The most 'tricky' part here is the didUpdateWidget section, which is needed to prevent an infinite update loop from happening.

This ReactiveTextField widget will manage its controller so the parent can remain a StatelessWidget. All you have to do as the user is set the current value and listen to the updates, THAT IS IT!

Full example

Check out this dartpad link for a full example!

Let me know what you think!