skip to content

In Angular reactive forms, what does FormBuilder add over calling new FormGroup() directly, and when would you inject NonNullableFormBuilder instead?

level: middleimportance: should knowfreq 50%

answer

  1. shorthand, not a new model
  2. value, [value, validators], {value, disabled}
  3. inject(FormBuilder).nonNullable
  4. reset() returns to the initial value
  5. existing controls are left alone

basics

~20 s

FormBuilder is an automatically provided service that turns plain values and arrays into FormControl, FormGroup, FormRecord and FormArray instances; it adds no new behaviour. NonNullableFormBuilder builds every implicit control with nonNullable: true, so reset() restores initial values instead of null.

solid answer

~40 s

`FormBuilder` produces exactly the same classes you would write by hand; it only shortens the syntax. In `fb.group({...})` each entry can be a raw value, an array `[value, validators, asyncValidators]`, a `{value, disabled}` state object, or an existing control, which is passed through as is. It also offers `fb.control()`, `fb.array()` and `fb.record()`. `NonNullableFormBuilder`, injected directly or reached as `inject(FormBuilder).nonNullable`, creates every implicit control with `nonNullable: true`: its type excludes `null` and `reset()` returns it to its initial value rather than `null`. That is usually what a shipping form wants, so many teams inject `NonNullableFormBuilder` by default. Controls you construct yourself inside the builder keep their own options, so a `new FormControl('')` inside `nnfb.group()` stays nullable.

code

ts · 29 lines
ts
import {Component, inject} from '@angular/core';
import {NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms';

@Component({
  selector: 'app-shipping-form',
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form">
      <input formControlName="recipient" />
      <div formGroupName="address">
        <input formControlName="city" />
        <input formControlName="postcode" />
      </div>
      <button type="button" (click)="form.reset()">Clear</button>
    </form>
  `,
})
export class ShippingForm {
  private readonly fb = inject(NonNullableFormBuilder);

  readonly form = this.fb.group({
    recipient: ['', Validators.required],
    address: this.fb.group({
      city: ['', Validators.required],
      postcode: '',
    }),
  });
  // form.reset() restores '' everywhere instead of null
}

go deeper

for a junior

Know how to inject FormBuilder and write fb.group() with raw values and [value, validators] arrays.

for a middle

Explain that the builder is sugar over the same classes, and that NonNullableFormBuilder makes reset() restore initial values.

for a senior

Spot the mixed-style bug where a hand-built control inside a non-nullable group resets to null, and standardise on one style per form.

for a principal

Set the team default builder and form conventions early, since nullable versus non-nullable choices ripple into every submit and reset path.

## A shorthand, not a different model Reactive forms can be written entirely with constructors: `new FormGroup({city: new FormControl('')})`. **`FormBuilder`** is an automatically provided service that writes those constructors for you. It creates the **same** `FormControl`, `FormGroup`, `FormRecord` and `FormArray` instances; there is no builder-specific behaviour at runtime. Its value is readability in large forms. | Builder call | Equivalent | |---|---| | `fb.control('FR')` | `new FormControl('FR')` | | `fb.group({city: ''})` | `new FormGroup({city: new FormControl('')})` | | `fb.array(['a', 'b'])` | `new FormArray([new FormControl('a'), new FormControl('b')])` | | `fb.record({})` | `new FormRecord({})` | ## What a group entry can be Inside `fb.group({...})` and `fb.array([...])`, each entry is interpreted: 1. **A raw value**, such as `''` or `'FR'`: shorthand for `fb.control(value)`. 2. **An array** `[value, validators?, asyncValidators?]`: a control with validators, such as `['', Validators.required]`. 3. **A state object** `{value: 'FR', disabled: true}`: a control created disabled. 4. **An existing `AbstractControl`**: passed through unchanged. Group-level options, such as validators on the whole group, go in the second argument of `fb.group()`. ## NonNullableFormBuilder By default a `FormControl` is **nullable**: its type includes `null`, and `reset()` without an argument sets it to `null`. For most business forms that is wrong: a reset shipping form should show empty strings or its defaults, not `null`. A control created with `{nonNullable: true}` instead resets to its **initial value**, and its type excludes `null`. `NonNullableFormBuilder` applies that option to every control **it** creates: - Inject it directly: `inject(NonNullableFormBuilder)`, or reach it from the regular builder: `inject(FormBuilder).nonNullable`. - `nnfb.group({city: ''})` then produces a `FormControl<string>` that resets to `''`. - **Existing controls are not altered.** If you write `nnfb.group({city: new FormControl('')})`, that inner control was constructed by you and stays nullable. Mixing the two styles in one group is a common source of an unexpected `null` after `reset()`. The type-level details of these controls, including how group values are inferred, belong to the typed-forms topic; the practical rule here is "prefer the non-nullable builder unless a field really can be `null`". ## Choosing between them - **Small forms, or a single control**: constructors are clear and need no injection. - **Larger forms**: the builder's shorthand keeps the structure readable, especially with validators. - **Most application forms**: `NonNullableFormBuilder`, so resets behave and types stay free of `null`. - **Legacy untyped code**: `UntypedFormBuilder` exists as an escape hatch while migrating to typed forms; do not use it for new code. ## Using it in a standalone component Because the builder is provided automatically, a standalone component obtains it with `inject()` in a field initializer, and can build the form in the same place: ```ts private readonly fb = inject(NonNullableFormBuilder); readonly form = this.fb.group({ recipient: ['', Validators.required], address: this.fb.group({city: ['', Validators.required], postcode: ''}), }); ``` No module import is needed for the builder itself; `ReactiveFormsModule` is still needed in the component's `imports` for the template directives. ## Common mistakes - Injecting `FormBuilder` and expecting non-nullable controls: the regular builder creates nullable controls; use `.nonNullable` or inject `NonNullableFormBuilder`. - Writing the array entry in the wrong order: it is `[value, validators, asyncValidators]`, value first. - Passing validators for the whole group as a child entry instead of in the second argument of `fb.group()`. - Mixing hand-built controls into a non-nullable group and being surprised by `null` after `reset()`. ## Interview framing Say that `FormBuilder` is syntax sugar over the same classes, list the entry forms it accepts, and explain `NonNullableFormBuilder` through its visible effect, `reset()` returning to defaults instead of `null`. Mentioning the "existing controls are not altered" caveat shows you have used it in real forms.

  • Inside nnfb.group(), one field is written as new FormControl(''). What does it hold after form.reset()?
    `null`. `NonNullableFormBuilder` only sets `nonNullable: true` on controls it creates from raw values, arrays or state objects. A control you construct yourself is passed through unchanged, and a plain `new FormControl('')` is nullable, so it resets to `null`. Write the entry as `''` or pass `{nonNullable: true}` to the constructor.
  • Does using FormBuilder make a form faster or change how validation runs?
    No. The builder only constructs ordinary `FormControl`, `FormGroup` and `FormArray` instances at creation time. After that, values, validation and change streams behave exactly as if you had written the constructors yourself; the choice is purely about readability.

saying these in an interview costs you the question

  • FormBuilder creates special builder-only control classes
  • NonNullableFormBuilder also changes controls you construct yourself
  • reset() on a default FormControl restores its initial value
  • FormBuilder must be added to the component's providers
  • The array entry form is [validators, value]