4
4
OnDestroy ,
5
5
Pipe ,
6
6
PipeTransform ,
7
+ untracked ,
7
8
} from '@angular/core' ;
8
9
import {
9
10
RxStrategyNames ,
@@ -165,7 +166,7 @@ export class RxPush implements PipeTransform, OnDestroy {
165
166
166
167
/** @internal */
167
168
ngOnDestroy ( ) : void {
168
- this . subscription ?. unsubscribe ( ) ;
169
+ untracked ( ( ) => this . subscription ?. unsubscribe ( ) ) ;
169
170
}
170
171
171
172
/** @internal */
@@ -179,26 +180,38 @@ export class RxPush implements PipeTransform, OnDestroy {
179
180
private handleChangeDetection ( ) : Unsubscribable {
180
181
const scope = ( this . cdRef as any ) . context ;
181
182
const sub = new Subscription ( ) ;
182
- const setRenderedValue = this . templateValues$ . subscribe ( ( { value } ) => {
183
- this . renderedValue = value ;
184
- } ) ;
185
- const render = this . hasInitialValue ( this . templateValues$ )
186
- . pipe (
187
- switchMap ( ( isSync ) =>
188
- this . templateValues$ . pipe (
189
- // skip ticking change detection
190
- // in case we have an initial value, we don't need to perform cd
191
- // the variable will be evaluated anyway because of the lifecycle
192
- skip ( isSync ? 1 : 0 ) ,
193
- // onlyValues(),
194
- this . render ( scope ) ,
195
- tap ( ( v ) => {
196
- this . _renderCallback ?. next ( v ) ;
197
- } )
183
+
184
+ // Subscription can be side-effectful, and we don't want any signal reads which happen in the
185
+ // side effect of the subscription to be tracked by a component's template when that
186
+ // subscription is triggered via the async pipe. So we wrap the subscription in `untracked` to
187
+ // decouple from the current reactive context.
188
+ //
189
+ // `untracked` also prevents signal _writes_ which happen in the subscription side effect from
190
+ // being treated as signal writes during the template evaluation (which throws errors).
191
+ const setRenderedValue = untracked ( ( ) =>
192
+ this . templateValues$ . subscribe ( ( { value } ) => {
193
+ this . renderedValue = value ;
194
+ } )
195
+ ) ;
196
+ const render = untracked ( ( ) =>
197
+ this . hasInitialValue ( this . templateValues$ )
198
+ . pipe (
199
+ switchMap ( ( isSync ) =>
200
+ this . templateValues$ . pipe (
201
+ // skip ticking change detection
202
+ // in case we have an initial value, we don't need to perform cd
203
+ // the variable will be evaluated anyway because of the lifecycle
204
+ skip ( isSync ? 1 : 0 ) ,
205
+ // onlyValues(),
206
+ this . render ( scope ) ,
207
+ tap ( ( v ) => {
208
+ this . _renderCallback ?. next ( v ) ;
209
+ } )
210
+ )
198
211
)
199
212
)
200
- )
201
- . subscribe ( ) ;
213
+ . subscribe ( )
214
+ ) ;
202
215
sub . add ( setRenderedValue ) ;
203
216
sub . add ( render ) ;
204
217
return sub ;
0 commit comments