angularlocal-storagesignalsangular-signals

Why is my Angular signal not populating with data from localStorage?


I am using the following function to add user data to cartSignal and store the data to localStorage. It works fine.

public addToCart = (item:CartItem) => {
    this.cartSignal.set({cartItems:[...this.cartSignal().cartItems, item]})
    console.log('cartSignal', this.cartSignal())
    saveStateToLocalStorage(this.cartSignal())
  }

This is the function that retrieves the data from localStorage:

export class AppService {
  public appSignal = signal<{ wayfair2:{cartItems:CartItem[]}}>({wayfair2:{cartItems:  []}})

  public restoreStateFromLocalStorage = () => {
    const wayfair2 = JSON.parse(localStorage.getItem("wayfair2") as string);  
    this.appSignal.set({ wayfair2}) 
    console.log('appSignal', this.appSignal())
  }
}

I encounter a problem when restoring cartSignal from localStorage. This is the effect that fires to repopulate cartSignal.

private appEffect = effect(() => {    
    let cartItems:CartItem[] = this.appService.appSignal().wayfair2.cartItems;
    console.log('cartItems', cartItems)
      this.cartSignal.set({cartItems:[...cartItems]})
      console.log('cartSignal.cartItems', this.cartSignal().cartItems)
 });

It seems to fail on this line this.cartSignal.set({cartItems:[...cartItems]})

The error is not discernible:

ERROR Error: NG0600
w http://localhost:4200/main-3YTPAYWP.js:3
II http://localhost:4200/main-3YTPAYWP.js:7
Ep http://localhost:4200/main-3YTPAYWP.js:1
wc http://localhost:4200/main-3YTPAYWP.js:1
set http://localhost:4200/main-3YTPAYWP.js:7
<anonymous> http://localhost:4200/main-3YTPAYWP.js:8
runEffect http://localhost:4200/main-3YTPAYWP.js:7
watcher http://localhost:4200/main-3YTPAYWP.js:7
a http://localhost:4200/main-3YTPAYWP.js:1
run http://localhost:4200/main-3YTPAYWP.js:7
flushQueue http://localhost:4200/main-3YTPAYWP.js:7
flush http://localhost:4200/main-3YTPAYWP.js:7
invoke http://localhost:4200/polyfills-FFHMD2TL.js:1
onInvoke http://localhost:4200/main-3YTPAYWP.js:7
invoke http://localhost:4200/polyfills-FFHMD2TL.js:1
run http://localhost:4200/polyfills-FFHMD2TL.js:1
flush http://localhost:4200/main-3YTPAYWP.js:7
scheduleEffect http://localhost:4200/main-3YTPAYWP.js:7
invokeTask http://localhost:4200/polyfills-FFHMD2TL.js:1
onInvokeTask http://localhost:4200/main-3YTPAYWP.js:7
invokeTask http://localhost:4200/polyfills-FFHMD2TL.js:1
runTask http://localhost:4200/polyfills-FFHMD2TL.js:1
$ http://localhost:4200/polyfills-FFHMD2TL.js:1
promise callback*H http://localhost:4200/polyfills-FFHMD2TL.js:1
U http://localhost:4200/polyfills-FFHMD2TL.js:1
scheduleTask http://localhost:4200/polyfills-FFHMD2TL.js:1
onScheduleTask http://localhost:4200/polyfills-FFHMD2TL.js:1
scheduleTask http://localhost:4200/polyfills-FFHMD2TL.js:1
scheduleTask http://localhost:4200/polyfills-FFHMD2TL.js:1
scheduleMicroTask http://localhost:4200/polyfills-FFHMD2TL.js:1
o http://localhost:4200/polyfills-FFHMD2TL.js:2
then http://localhost:4200/polyfills-FFHMD2TL.js:2

Solution

  • The error corresponds to setting a signal inside an effect.

    NG0600: Writing to signals is not allowed in a computed or an effect by default. Use allowSignalWrites in the CreateEffectOptions to enable these inside effects.

    To solve this problem, you can make use of untracked function which according to docs:

    Execute an arbitrary function in a non-reactive (non-tracking) context. The executed function can, optionally, return a value.

    Thus, by setting the signal inside this wrapper function, it does not trigger this error, since its a non-reactive (non-tracking) context, the effect does not track the update of this signal.

    private appEffect = effect(() => {    
        let cartItems:CartItem[] = this.appService.appSignal().wayfair2.cartItems;
        console.log('cartItems', cartItems)
          untracked(() => { // <- changed here!
            this.cartSignal.set({cartItems:[...cartItems]})
          });
          console.log('cartSignal.cartItems', this.cartSignal().cartItems)
     });
    

    The update method of the signal, is a great function, when you need the current state of the signal, we can change the addToCart function to.

      public addToCart = (item:CartItem) => {
        this.cartSignal.update((cartSignalData: any) => ({
          cartItems: [...cartSignalData.cartItems, item]
        }));
        console.log('cartSignal', this.cartSignal())
      }
    

    The effect method is an awesome way to keep the signal in sync with local storage, with less code.

    effect(() => {
      const cartSignalData = this.cartSignal();
      saveStateToLocalStorage(this.cartSignal())
    }
    

    Whenever the signal updates, it is guaranteed to be stored in local storage, so you do not have any extra code for this. It reacts on it's own. But we need to make sure objects are set with new references each time.