In Angular reactive forms, what does FormBuilder add over calling new FormGroup() directly, and when would you inject NonNullableFormBuilder instead?
answer
- shorthand, not a new model
- value, [value, validators], {value, disabled}
- inject(FormBuilder).nonNullable
- reset() returns to the initial value
- existing controls are left alone
basics
~20 sFormBuilder 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 linesimport {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
Know how to inject FormBuilder and write fb.group() with raw values and [value, validators] arrays.
Explain that the builder is sugar over the same classes, and that NonNullableFormBuilder makes reset() restore initial values.
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.
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]