8000 "Input is required but no value is available yet" with toObservable if the content is dynamic · Issue #59067 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

"Input is required but no value is available yet" with toObservable if the content is dynamic #59067

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
csisy-bt4w opened this issue Dec 5, 2024 · 37 comments
Assignees
Labels
area: core Issues related to the framework runtime area: forms core: reactivity Work related to fine-grained reactivity in the core framework cross-cutting: signals state: needs more investigation
Milestone

Comments

@csisy-bt4w
Copy link

Which @angular/* package(s) are the source of the bug?

Don't known / other

Is this a regression?

Yes

Description

Just updated to Angular v19 and started to experience an error. Created an example with as minimal code as possible. Unfortunately it doesn't make too much sense without additional context, but it is what it is.

So the setup is the following:

  • We have a directive that has a required input (ModelDirective)
  • We have a component that queries a ModelDirective and uses its required input (FormFieldComponent)
  • We also need to turn that input signal into an rxjs observable (for reasons that is not obvious by the example)

The problem is observed within the toObservable but only if the queried directive is created dynamically within a dynamically created component. If you run the example, you can see that the input is actually set properly - if we directly use the computed signal, its value is successfully displayed. However, once we try to read the observable's value, an error is thrown.

If, however, the directive or the owning component is available without any condition, everything works fine (there is a commented line in the example template).

This setup was working fine in Angular v18.

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/stackblitz-starters-vjskep?file=src%2Fmain.ts

Please provide the exception or error you saw

ERROR RuntimeError: NG0950: Input is required but no value is available yet.

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 19.0.3
Node: 20.11.1
Package Manager: npm 10.2.4
OS: win32 x64

Angular: 19.0.3
... animations, cli, common, compiler, compiler-cli, core
... localize, platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1800.3
@angular-devkit/build-angular   19.0.3
@angular-devkit/core            18.0.3
@angular-devkit/schematics      18.2.11
@angular/cdk                    19.0.2
@schematics/angular             18.2.11
ng-packagr                      19.0.1
rxjs                            7.8.1
typescript                      5.5.4
zone.js                         0.15.0

Anything else?

No response

@ngbot ngbot bot added this to the needsTriage milestone Dec 5, 2024
@JeanMeche JeanMeche added area: core Issues related to the framework runtime core: reactivity Work related to fine-grained reactivity in the core framework cross-cutting: signals labels Dec 5, 2024
@JeanMeche
Copy link
Member
JeanMeche commented Dec 5, 2024

A narrowed down repro: https://stackblitz.com/edit/stackblitz-starters-u3sbf6?file=src%2Fmain.ts

@csisy-bt4w
Copy link
Author
csisy-bt4w commented Dec 6, 2024

Thank you, that is much more simple. I've tried to reproduce it with effect but I have must done something differently.

Just as a side note, which is somewhat obvious after you narrowed down the example, if we use ɵmicrotaskEffect instead of effect, there is no error.

@BurscherChris
Copy link
BurscherChris commented Dec 13, 2024

I encountered the same issue with dynamic content in a loop.

creatable(); is an signal from my signalStore

  <app-select [ngModel]="selected()?.key" (ngModelChange)="select($event)" required>
    @for (month of creatable(); track month.key) {
      @if (month) {
        <app-option
          [key]="month.key"
          [label]="month.month"
        />
      }
    }
  </app-select>

Root Cause:
The issue occurs because Angular expects the [key] binding in the app-option component to have a value ready when the component is instantiated. However, with dynamic data in a loop, some values might not be immediately available or might be undefined/null at the time of rendering. This causes the error: "Input is required but no value is available yet."

Add an *ngIf Condition
Ensure the app-option component is only rendered if all required data for the binding is available:

@for (month of creatable(); track month.key) {
  @if (month?.key && month?.month && month?.year) {
    <app-option
      [key]="month.key"
      [label]="month.month"
    />
  }
}

@e-oz
Copy link
e-oz commented Dec 13, 2024

It can be narrowed even further:
https://stackblitz.com/edit/stackblitz-starters-o3eivrnt?devToolsHeight=33&file=src%2Fmain.ts

@Component({
  selector: 'my-app',
  template: `<app-child id="5"/>`,
  imports: [ChildComponent],
})
export class App {}

@Component({
  selector: 'app-child',
  template: ``,
})
export class ChildComponent {
  id = input.required<string>({ alias: 'id' });

  constructor() {
    const err = this.id();
  }
}

@JeanMeche
Copy link
Member

@e-oz Reading a required input will always fail in the constructor. Your example exhibit an expected behavior.

@keatkeat87
Copy link
keatkeat87 commented Dec 15, 2024
<!--OK-->
<app-parent>
  <app-child value="a" />
  <app-child value="b" />
  <app-child value="c" />
</app-parent>
<!--
 parent.component.ts
 constructor() {
   effect(() => console.log('effect', this.children()[0].value())); 
 }
  
 note: read child.value() in effect is OK
-->

<!--Error-->
<app-parent>
  @for (value of ['a', 'b', 'c']; track value) {
    <app-child [value]="value" />
  }
</app-parent>
<!--
 parent.component.ts
 constructor() {
   effect(() => console.log('effect', this.children()[0].value())); 
 }
  
 note: read child.value() in effect will error
-->

because of

Image

Why isn't "runEffectsInView" run after "detectChangesInEmbeddedViews"?
Is this by design?
I'm very puzzled 🤔.
If "detectChangesInEmbeddedViews" is not called, the child input will be undefined. If it's marked as required, an error will occur.

@paulflo150
Copy link

Has anyone been able to come up with a workaround for this issue? Since it hasn't been marked as a bug I'm assuming it won't get fixed any time soon.

@e-oz
Copy link
e-oz commented Dec 17, 2024

The workaround is simple:

private readonly injector = inject(Injector);
protected readonly displayValue$ = defer(() =>
    toObservable(this.displayValue, { injector: this.injector }).pipe(
      startWith(null)
    )
  );

https://stackblitz.com/edit/stackblitz-starters-aapwxsuc?file=src%2Fmain.ts

@keatkeat87
Copy link

Has anyone been able to come up with a workaround for this issue? Since it hasn't been marked as a bug I'm assuming it won't get fixed any time soon.

call effect inside AfterContentInit lifecycle hook.

@csisy-bt4w
Copy link
Author

The workaround is simple:

private readonly injector = inject(Injector);
protected readonly displayValue$ = defer(() =>
toObservable(this.displayValue, { injector: this.injector }).pipe(
startWith(null)
)
);
https://stackblitz.com/edit/stackblitz-starters-aapwxsuc?file=src%2Fmain.ts

The only caveat here is that on every subscription a new observable will be created, so be careful when using it.

@e-oz
Copy link
e-oz commented Dec 18, 2024

@csisy-bt4w as happens with any cold observable. Use shareReplay() if you want multiple subscriptions (and not only in this case).

@csisy-bt4w
Copy link
Author

@e-oz You're somewhat right, even the non-deferred one's pipeline is re-executed if not shared. However, just wanted to point out that with defer the observable instance itself is re-created, so be careful where you put the share or shareReplay. So for example:

private readonly injector = inject(Injector);

private readonly source = signal(42);
private readonly source$ = defer(() =>
  toObservable(this.source, { injector: this.injector }).pipe(
    tap((val) => console.log('value', val)),
    shareReplay({ bufferSize: 1, refCount: true }), // <-- shareReplay here
  )
);

ngOnInit(): void {
  this.source$.subscribe((val) => console.log('sub1', val));
  this.source$.subscribe((val) => console.log('sub2', val));
}

That shareReplay there has little to no effect. It must be put after the defer.

https://stackblitz.com/edit/stackblitz-starters-7rhpx4gd?file=src%2Fmain.ts

@e-oz
Copy link
e-oz commented Dec 19, 2024

@csisy-bt4w that's correct!

@paulflo150
Copy link

Has anyone been able to come up with a workaround for this issue? Since it hasn't been marked as a bug I'm assuming it won't get fixed any time soon.

call effect inside AfterContentInit lifecycle hook.

This is not a workaround unless you're maybe dealing with static data. It will not work when the data is loaded async.

@keatkeat87
Copy link
keatkeat87 commented Dec 22, 2024

This is not a workaround unless you're maybe dealing with static data. It will not work when the data is loaded async.

I'm not quite sure about the scenario you're referring to. Could you provide a more detailed explanation? I'd be happy to discuss it with you.

@JusuVh
Copy link
JusuVh commented Jan 23, 2025

I'm not quite sure about the scenario you're referring to. Could you provide a more detailed explanation? I'd be happy to discuss it with you.

Simple: if you have a select component with options, your workaround works for the retieval of initial options. However, if the options change programatically, after the component was created, the options retrieved in the effect are not 'complete', as mentionned above. They do not have their new [id] inputs for instance (values are undefined, and if the inputs are required, you get the error).

One simple workaround I found is to put the code inside the effect in a setTimeout(). I hate this solution, but it seems to work for me.


The major problem I have, that is somewhat related to the original problem, is that the behaviour is different between v18 and v19.

In v18, @ContentChildren and contentChildren() had the same behavior:
retrieving values from content with @ContentChildren with

this.contentChildren.changes.pipe(startWith(this.contentChildren)).subscribe(value => ....)

and retrieving them from the contentChildren() signal like

effect(() => {
  const contentChildren = this.contentChildren();
  ....
})

would get me the same values, with the same child components after the were initialized (form what I can guess, after the OnInit hook).

In v19, the signal now works differently from the old @ContentChildren notation. The signal gets the child components AFTER they are created but BEFORE they are initialized, so any input (like a static [value] or [id] input) is not available in the contentChildren signal when an effect or computed is triggered. This creates the 'Input required' problem.
So the values you get from the contentChildren() signal are not the same as the ones you used to get from the @ContentChildren changes observable.

Is this really by design ? I don't really see the use case of wanting the children as 'empty shells' and NOT initialized.

If this is the case, I will migrate back to using @ContentChildren because it seems quite complex to get the child components 'the old way' with the contentChildren signal.

@JusuVh
Copy link
JusuVh commented Jan 23, 2025

You can reproduce the problem here :

https://stackblitz.com/edit/stackblitz-starters-ww25jcsa?file=src%2Fmain.ts

You can see in the console that the option values are undefined when the contentChildren signal is triggered.
If you add 'required' to the value inputs in the option component, the 'Input required' error is produced instead.

Edit: I updated the stackblitz to include an 'old way' @ContentChildren and a button to change the children programatically.

Edit2: here is the same exact code running with Angular 18.2
https://stackblitz.com/edit/stackblitz-starters-f2q1mgss?file=src%2Fmain.ts

@Abreuvoir
Copy link

We had the exact same use case than @JusuVh and the workaround we used to get children options inputs was to move the effect creation inside afterContentInit instead of the constructor.
We didn't have any issues in 18.2.
It would be nice to know if it's the intended behavior now

@JusuVh
Copy link
JusuVh commented Jan 23, 2025

move the effect creation inside afterContentInit instead of the constructor

As I mentioned above, this works for the initial content creation but if the content changes after that, the signal returns 'empty' child components (see button in stackblitz above).

@keatkeat87
Copy link
keatkeat87 commented Jan 23, 2025

If I didn't misunderstand what you guys meant,
JusuVh is correct.
Executing the effect after afterContentInit can only delay the first time effect. The same issue will occur the second time.
I have mentioned the reason above, based on v19 source code, the effect will be executed before the @for render.
I still think it's just that the Angular Team wrote the order incorrectly. It shouldn't by design.

@JusuVh
Copy link
JusuVh commented Jan 31, 2025

Is there anything news on this ?

I'm building / maintaining a component library and this is quite a problematic regression for many group components that have child items with required value inputs, like radios, switchs, selects and so on.

To fix the issue, I have to basically wrap all code run in effects is a setTimeout, like so :

effect(() => {
  const items = this.childContent();

  untracked(() => setTimeout(() => 
    ...
  ));
});

I have not found any better way to consistently and easily fix the problem.
The Material library has not been migrated to the new content signals so I guess they don't have any issue on their side.

Should I open a new issue specifically with the examples I showed above ?

@JeanMeche
Copy link
Member

v19 introduced timing changes for effect. effect runs before a component in checked the a CD cycle. Which explains why the queries are undefined. If you want an effect that runs after CD, use afterRenderEffect.

@JusuVh
Copy link
JusuVh commented Jan 31, 2025

If you want an effect that runs after CD, use afterRenderEffect.

I see, thank you for the clarification. So you can confirm that this is indeed by design?
For me, the problem is not so much with the effect running too early as it is with the contentChildren signal triggering too early, before the content is correctly initialiszed (but I might be missing something).

And what I also find strange is that the behaviour is not consistent between using direct content projection or a @for loop (with @ContentChildren notation and the changes observable, it was consistent).

Edit: and to repeat what I said earlier, what's the use case of wanting the contentChildren before they are initialized, because they are, as far as I'm concerned, unusable ?

@keatkeat87
Copy link

JeanMeche, could you confirm if this behavior is by design?

If yes, can you explain in more detail why Angular doesn’t run effects after detectChangesInEmbeddedViews?

before signal-based, we rely on lifecycle hooks ngAfterContentInit and ngAfterViewInit. If Angular were to run effects before detectChangesInEmbeddedViews, we would still need ngAfterContentInit. However, if effects were run after detectChangesInEmbeddedViews, the effect could replace ngAfterContentInit.

The afterRenderEffect is used for DOM manipulation, and it is typically used to replace ngAfterViewInit.

< 6D40 a class="d-inline-block" data-hovercard-type="user" data-hovercard-url="/users/JeanMeche/hovercard" data-octo-click="hovercard-link-click" data-octo-dimensions="link_type:self" href="/JeanMeche">@JeanMeche
Copy link
Member

I've have some explainations about the changes in v19 in this article: https://riegler.fr/blog/2024-10-15-effect-context/

afterRenderEffect is the effect that runs after CD.

afterRenderEffect [...] is typically used to replace ngAfterViewInit.

afterNextRender would be the right replacement for ngAfterViewInit.

@keatkeat87
Copy link

afterRenderEffect and afterNextRender occur at the same time, right?
as both run after refreshView when all DOM updates are complete.
If we want to perform some DOM manipulation, that would be good timing.
However, that's not my focus right now.

Before signal-based, we had afterContentInit and QueryList for detecting content changes. We listened for changes not necessarily for DOM manipulation, but perhaps to update state. Therefore, afterRenderEffect isn’t as suitable for that compared to effect. Ideally, we want to use effect to replace afterContentInit/QueryList while maintaining the same timing.

@JeanMeche
Copy link
Member

afterRenderEffect() is not only for DOM manipulations but for any case where an effect needs to run after rendering.
Having children is exactly that.

@JusuVh
Copy link
JusuVh commented Jan 31, 2025

Ok, I played with afterRenderEffect a bit and it works, but I find this much more confusing than either the old @ContentChildren with changes observable or a simple effect where I can use the contentChildren() signal and other internal signals at the same time.

Maybe I need to sleep on it...

I would have prefered something to control the behaviour in the contentChildren() signal itself, like a 'afterRender: true' option, to control when the signal triggers, rather than having to think when the signal should be read.

@keatkeat87
Copy link

JeanMeche, how did you define "after rendering"?

look at my above example
https://stackblitz.com/github/keatkeat87/ng-effect-timing-issue?file=README.md

there are 3 components, App, Parent, Child

export class AppComponent {
  protected readonly values = signal(['a', 'b', 'c']);
}
export class ParentComponent {
  private readonly children = contentChildren(ChildComponent);

  constructor() {
    effect(() => console.log(this.children().map(c => c.value())));
  }
}
export class ChildComponent {
  readonly value = input.required<string>();
}

App Template

Image

why the below code got the error?
is it because using @for makes it become "after rendering"?
it makes Angular users confused.

when we say "after rendering" (e.g., afterNextRender / afterViewChecked), it typically means after the component's LView has been refreshed. In the example above, the effect in the ParentComponent runs when the App's LView is being refreshed, but the Parent and Child LViews have not started refreshing yet. However, if we switch to "afterEffectRender," it would mean that all the Parent and Child LViews have completed their refresh.

@JeanMeche
Copy link
Member

This is more or less the discussion we're having in this issue. This is indeed confusing.

@keatkeat87
Copy link

Angular have 3 effect execution timing

root effect -- before all refreshView
view effect -- executing refreshView
afterEffectRender -- after all refreshView

I think they each have their own responsibilities, we must clarify their scope.

as I mentioned above,
Image
i think we should ask the code writer why he/she chose to run 'runEffectsInView' before 'detectChangesInEmbeddedViews'.
we have already seen its impact from the perspective of Angular users, so what is the different from the perspective of Angular Framework inside?

@TomaszStanik
Copy link
TomaszStanik commented Apr 10, 2025

Hi,

we faced this issue with migrating from Angular v18 to v19. This was for us unexpected behavior. I can agree with the @keatkeat87 abd @JusuVh.

Maybe we should be able to decide when we want to get event emitted from contentChildren while using toObservable?

In Angular v18 toObservable behavior while using static/dynamic content was predictable. Now, depends the content is dynamic or or not, toObservable is behaving different. Like previous talkers talks:

Case 1: Static Content

<parent>
   <child [value]="1" />
</parent>
@Component(...)
export class ChildComponent {
   value = model.required<any>();
}

@Component(...)
export class ParentComoponent {
  private readonly children = contentChildren(ChildComponent);

  constructor() {
    toObservable(this.children).subscribe((children) => {
      if (children.length > 0) {
        console.log(children[0].value()); // Works! .required is accessable from here
      }
    });
  }
}

Case 2: Dynamic Content:

<parent>
    @defer {
      <child [value]="1" />
    } @loading (minimum 1000ms) {
      Loading....
    }
</parent>
@Component(...)
export class ChildComponent {
   value = model.required<any>();
}

@Component(...)
export class ParentComoponent {
  private readonly children = contentChildren(ChildComponent);

  constructor() {
    toObservable(this.children).subscribe((children) => {
      if (children.length > 0) {
        console.log(children[0].value()); // Error! Input is required but no value is available
      }
    });
  }
}

As I said - this is unexpected and a little bit unlogical for our team. We need to convert back all contentChildren/viewChildren to @ViewChildren @ContentChildren and listen for .changes which is a big regression.

export class ParentComponent{
  @ContentChildren(ChildComponent)
  private children!: QueryList<ChildComponent>;

  private readonly destroyRef = inject(DestroyRef);

  constructor() {
    afterRenderEffect(() => {
      this.children.changes
        .pipe(takeUntilDestroyed(this.destroyRef))
        .subscribe(() => {
          const children = this.children2.toArray();

          if (children.length > 0) {
            console.log(children[0].value()); // Works.
          }
        });
    });
  }
}

EDIT:
This is a workaround function which is restoring previous behavior of toObservable.

function toObservableAfterRender<T>(signal: Signal<T>) {
  const destroyRef = inject(DestroyRef, {
    optional: true,
  });

  const subject = new Subject<T>();

  if (destroyRef != null) {
    destroyRef.onDestroy(() => {
      subject.complete();
    });
  }

  afterRenderEffect(() => {
    subject.next(signal());
  });

  return subject.asObservable();
}

Use case:

export class ParentComponent{
  private readonly children = contentChildren(ChildComponent);

  constructor() {
    toObservableAfterRender(this.children).subscribe((children) => {
      if (children.length > 0) {
        console.log(children[0].value()); // Now works
      }
    });
  }
}

@keatkeat87
Copy link

@alxhub
v20 "effect" is about to stabilize.
Is this issue not going to be addressed?

@TomaszStanik
Copy link

What with this issue? This is a huge breaking-change with basic behaviors of angular which is forcing to remove .required marker from all inputs and models.

#61214

I'm still confusing why no one from Angular Team didn't explain this behavior since Dec 2024.

@alxhub
Copy link
Member
alxhub commented May 14, 2025

@alxhub v20 "effect" is about to stabilize. Is this issue not going to be addressed?

It's not really an issue with effect's timing, but the fact that queries return results as soon as they exist, which does not guarantee that those results have been initialized with their input values.

This is something we can look into fixing on the query side. A workaround is to filter out uninitialized results yourself, by e.g. adding an isInit signal in the child components and setting it in their ngOnInit.

@keatkeat87
Copy link
keatkeat87 commented May 14, 2025

@alxhub, thank for reply.

but I think you’re not very clear about the situation.

I'm not sure whether you have time to discuss this issue in more depth.

Since effects will be stabilized in v20, I just don’t want the issue to end up being solved in an incorrect way due to the restriction of no breaking changes in the future.

I'm not sure if you’ve looked at all the examples I mentioned above? but if you’re willing to continue the discussion after JeanMeche last reply, I’d be happy to do so.

@e-oz
Copy link
e-oz commented May 14, 2025

Is it possible that this is happening synchronously (without micro/macro tasks):

  1. The class is created, fields are initialized, input.required() is called — the special symbol is set as the value;
  2. Other fields are initialized — they might read the required input;
  3. The constructor() is called — it might read the required input;
  4. Finally, the required input is initialized with the actual value.

If my guess is incorrect, please ignore my question.
If it's correct, could you please change createInputSignal so that it not only creates a signal,
but also reads the initial value from the parent component or the router?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: core Issues related to the framework runtime area: forms core: reactivity Work related to fine-grained reactivity in the core framework cross-cutting: signals state: needs more investigation
Projects
None yet
Development

No branches or pull requests

0