Skip to content

Conversation

@HadiAlMarzooq
Copy link

Fixes #540

The :for loop directive wasn't reactively tracking array property access when arrays transitioned from empty to populated. The effect generator passed component.items directly as a function argument (forloops[1](component.items, ...)), causing the reactive proxy's get handler to potentially miss the dependency tracking during argument evaluation. By storing the property in a local variable (const collection = component.items) before passing it to the forloop function, we ensure the property access occurs explicitly during effect execution with the correct currentEffect and currentKey context, guaranteeing track() is invoked and the dependency is registered. This addresses the same issue mentioned in v0.9.4 CHANGELOG, which suggests the previous fix didn't fully resolve the tracking mechanism. All tests updated and passing (920 tests).

@CLAassistant
Copy link

CLAassistant commented Nov 20, 2025

CLA assistant check
All committers have signed the CLA.

@github-actions
Copy link

Test Results: ✅ PASSED

Run at: 2025-11-20T16:28:19.296Z

Summary:
passed: 920 failed: 0 of 920 tests

@michielvandergeest
Copy link
Collaborator

@HadiAlMarzooq awesome, this looks good! Thanks for contributing this.

I'm wondering why this change would ensure the tracking isn't being missed though 🤔 - maybe it happens with certain transpiled code?

just for my own sanity, do you have a clear reproduction scenario of when the issue happens?

@HadiAlMarzooq
Copy link
Author

Thanks @michielvandergeest , I'll try to give a clear reproduction scenario and what we discovered during debugging:

Reproduction Scenario

The issue occurs when a component has a prop that starts as an empty array [] and gets populated asynchronously (e.g., via API call), and the :for loop uses a computed property that depends on that prop.

In our app, we have a Row component that receives a cards prop:

// Row.js
export default Blits.Component('Row', {
  props: ['cards'],
  
  computed: {
    processedCards() {
      return this.cards.map(card => {
        // ... process card data
        return processedCard
      })
    }
  },
  
  template: `
    <ContentCard
      :for="(card, index) in $processedCards"
      :key="$card.id"
    />
  `
})

The Problem: Initially cards = [] (empty). After async load, cards = [{...}, {...}, {...}] (populated). The :for loop doesn't re-render, even though processedCards computed property updates correctly. Component-level watchers fire, state updates correctly, but the :for loop effect doesn't re-run.

What We Found During Debugging

After deep debugging, I discovered the root cause is in how the effect tracks dependencies when using computed properties.

When the effect was registered with a specific key filter like effect(eff, ['processedCards']), the tracking system would only track accesses to the 'processedCards' key. However, when the computed property's getter runs and accesses this.cards, that access goes through the component's prop getter, which accesses this[symbols.props]['cards']. Since this[symbols.props] is a reactive proxy, accessing this[symbols.props]['cards'] should trigger track(this[symbols.props], 'cards'). But because currentKey was set to ['processedCards'], the track() function would check if 'cards' is in that array, and since it's not, it would skip tracking that access (see line 54 in effect.js: if (currentKey.includes(key) === false) return).

This means the effect was only tracking the computed property name itself, not the underlying reactive properties that the computed depends on. When cards changed from [] to [{...}], the reactive system would trigger trigger(this[symbols.props], 'cards'), but since the effect wasn't registered to track that key, it wouldn't re-run.

The Fix

By changing the effect registration to use null as the currentKey parameter (effect(eff, null)), we tell the tracking system to track all property accesses during effect execution, not just a specific key. This ensures that when the computed property's getter accesses this.cards (which goes through this[symbols.props]['cards']), that access is properly tracked. When cards changes, the effect re-runs and the :for loop re-renders.

I also added explicit access to underlying props/state for direct properties (not computed) to ensure tracking is established even when arrays start empty, though the main fix is the null currentKey.

Known Limitation

During testing, I discovered a limitation: Computed properties that depend on component state (not props) do not work reactively in :for loops. This appears to be a deeper issue with how state reactivity interacts with computed property getters.

  • Works: :for loops with computed properties depending on props (e.g., $processedCards where processedCards() accesses this.cards prop)
  • Doesn't work: :for loops with computed properties depending on state (e.g., $processedItems where processedItems() accesses this.items state)

Workaround: Use props instead of state for arrays that are used in computed properties within :for loops, or use direct state access in the :for loop instead of computed properties.

Test Results

I created a test page (/test/forloop) that reproduces this scenario. It shows that with the fix:

  • ✅ Direct :for loops work correctly
  • :for loops using computed properties with props work correctly (Test 3 - Row component)
  • :for loops using computed properties with state do not work (Test 2)

The fix is minimal and focused - it just changes how the effect tracks dependencies, ensuring computed property dependencies are properly tracked for props without affecting other functionality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants