Understanding InjectionContext and Signal Effects

Photo by CDC on Unsplash

Understanding InjectionContext and Signal Effects

A Simplified Explanation of Signal Effects NG0203 and NG0600

Yesterday, I was chatting with my friend Jörgen de Groot about Signals. As our conversation shifted to cover effects, he encountered the error ERROR NG0203.

Later, another friend approached me with the same issue. I decided to compose a brief article to explain why it happens.

Scenario

We have a counter app using signals to update the times of clicks, when the count signals variable gets 10, we must update a local variable (no signals) to 'You reached the limit of clicks'.

Using Effect

The signal effect is very useful for handling side effects. In our case, we have a side effect where the variable 'limitMessage' needs to be updated when the 'counts' variable reaches 10.

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
   <h1>{{limitMessage}}</h1>
    <h2>{{ count()}} times</h2>
 <button (click)="increase()">+1</button>
  `,
})
export class App  {
  count = signal(0);
  limitMessage = 'Feel free to click!';

  increase() {
    this.count.update((p) => p + 1);
  }
}
bootstrapApplication(App);

We run the effect in the ngOnInit lifecycle to track the changes in the count variables.

export class App implements OnInit {
  count = signal(0);
  limitMessage = 'Feel free to click!';

  ngOnInit(): void {
    effect(() => {
      if (this.count() == 10) {
        this.limitMessage = 'You reached the limit, sorry';
      }
    });
  }

Darn!!!! We got the error!!

Effects and Injection Context

The effect must be used in an injection context because it uses the DestroyRef service to unsubscribe when the building block is destroyed.

The Constructor

The first approach is to move the effect to the constructor, and save changes, yeah! the effect runs!!

Using runInjectionContext

To use the effect outside of the constructor, we must use the runInjectionContext function, by injecting the Injector.

export class App implements OnInit {
  count = signal(0);
  limitMessage = 'Feel free to click!';
  disabled = signal(false);

  #injector = inject(Injector);

  ngOnInit(): void {
    runInInjectionContext(this.#injector, () => {
      effect(() => {
        if (this.count() == 10) {
          this.limitMessage = 'You reached the limit, sorry';
        }
      });
    });
  }
  increase() {
    this.count.update((p) => p + 1);
  }
}

Yes! We have the effect outside of the class constructor! And no errors!

Update Signals In Effects

We are using effects without problems, but let's add a small feature connected to my effect. I want to disable the button when the count reaches 10.

I want to create the variable 'disabled' as a boolean signal and use the effect to update the value.

export class App implements OnInit {
  count = signal(0);
  limitMessage = 'Feel free to click!';
  disabled = signal(false);
  #injector = inject(Injector);

  ngOnInit(): void {
    runInInjectionContext(this.#injector, () => {
      effect(() => {
        if (this.count() == 10) {
          this.limitMessage = 'You reached the limit, sorry';
          this.disabled.update(() => true);
        }
      });
    });
  }
  increase() {
    this.count.update((p) => p + 1);
  }
}

Save changes then try to update the disabled signals!!! DAMM!!!!!!!!

By default, effects aren't allowed to update signals, but for these scenarios, we can disable this restriction by setting allowSignalWrite it to true.

Perfect! we are working with Signals effect without any problem!

Recap

We learn how to implement the effect in the ngOnInit lifecycle, explore the importance of the injection context, how to use runInInjectionContext to avoid errors, and update Signals in effects by disabling the default restriction using the allowSignalWrite option.

I hope this helps you when you start using Signals and effects! ;)

Source Code