#Effect Order

12 messages · Page 1 of 1 (latest)

young stratus
#

Is the order that effects run in guaranteed? If I do this:

  const resolvedKey = computed(() => this.name() + '-key');

  effect(() => {
    const stored = localStorage.getItem(resolvedKey());
    if (stored) state.set(JSON.parse(stored));
  });
  effect(() => localStorage.setItem(resolvedKey(), JSON.stringify(state()));

Is the first effect guaranteed to run before the second effect? If not, what is the angular way of making something like this work such that a computed property initializes the storage and ongoing updates update the value?

hard quartzBOT
#

Hi @young stratus. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

Effect Execution Order

The knowledge sources do not explicitly guarantee a specific ordering between multiple effects created in the same context. What is documented is:

  • "View effects" run before their corresponding component is checked during change detection.
  • "Root effects" run prior to all components being checked.
  • Effects always execute asynchronously, during the change detection process. [Angular Effects Guide]

There is no documented guarantee that one effect() call will always run before another effect() call defined after it.


The Recommended Angular Approach

More importantly, your design has a conceptual issue that the Angular docs directly address:

Avoid using effects for propagation of state changes. Instead, use computed signals to model state that depends on other state. [Angular Effects Guide]

The docs also note that effects are appropriate for keeping data in sync with storage (e.g., localStorage), but the read-from-storage part (initialization) is better handled outside of an effect entirely — for example, in the constructor or during service initialization.

Here's the Angular-idiomatic pattern based on the knowledge sources:

// Initialize state from localStorage directly (not in an effect)
const resolvedKey = computed(() => this.name() + '-key');

// Read once at initialization time — no effect needed
const stored = localStorage.getItem(resolvedKey());
const state = signal(stored ? JSON.parse(stored) : defaultValue);

// Use a single effect to keep localStorage in sync with state changes
effect(() => {
  localStorage.setItem(resolvedKey(), JSON.stringify(state()));
});
#

This way:

  1. The initial read from localStorage happens synchronously at construction time — no ordering concern.
  2. The write effect reactively keeps localStorage up to date whenever state or resolvedKey changes. [Angular Effects Guide]

This avoids the race condition entirely by not relying on effect ordering.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@young stratus Kapa.ai is still learning and improving, please let me know how I did by reacting below.

drifting stump
# young stratus Is the order that effects run in guaranteed? If I do this: ```ts const resolv...

I do not think so. I can't claim to have had to solve this before because I'd use signalstore for this, which has an onInit hook for pretty much this scenario.
In your case, you want to express that state should not be used by the second effect until the first effect has run once (at least I assume it's only supposed to run once).

I'd just have an "isInitialized" signal variable thusly

#

Though this kind of smells like a usecase for linkedsignal as well?
You want state to reset to the value from localstorage every time name changes, while still being able to manipulate state, with syncing to localstorage being an afterthought side-effect right?
Sounds to me like linkedsignal + effect

young stratus
#

@drifting stump linkedSignal is sort of what I'm looking for, but the difficulty is I want it to work for model too, so that I can persist model. I think linkedSignal could work though, acting as a declarative way to flip a loaded variable so that only one effect is needed to keep the state aligned:

export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
export interface PersistSignalOptions {
  injector?: Injector;
}

export const persistSignal = <T extends JsonValue>(
  writableSignal: WritableSignal<T>,
  key: string | (() => string),
  options?: PersistSignalOptions,
): WritableSignal<T> => {
  if (options?.injector) {
    return runInInjectionContext(options.injector, () =>
      persistSignal(writableSignal, key, { ...options, injector: undefined }),
    );
  }

  assertInInjectionContext(persistSignal);

  const derivedKey = typeof key === "function" ? key : () => key;
  const loadedStoredValue = linkedSignal({ source: derivedKey, computation: () => false });

  effect(() => {
    const currentKey = derivedKey();
    const currentSignal = writableSignal();

    if (!loadedStoredValue()) {
      loadedStoredValue.set(true);
      const storedValue = localStorage.getItem(derivedKey());
      if (storedValue !== null) writableSignal.set(JSON.parse(storedValue));
      return;
    }

    localStorage.setItem(currentKey, JSON.stringify(currentSignal));
  });

  return writableSignal;
};
idle kraken
#

(which hasn't landed yet)

young stratus
#

Yes, that's it exactly! Thanks for pointing that out! Looking forward to that being a thing

patent flicker
#

Got merged.