Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passing block param in component - modified twice in a single render #11519

Closed
typeoneerror opened this issue Jun 19, 2015 · 19 comments
Closed

Comments

@typeoneerror
Copy link

I'm not sure if this is related to #11277, but I'm experiencing a similar issue. I've been able to reproduce the issue on this jsbin.

The first two uses of {{editable-thing}} work fine, but the last where we pass through the isEditing param via yield starts the unending warning loop (fair warning, open Chrome's task manager so you can kill the "runaway process" when you click the third edit button).

Is this a bug or a misuse/misunderstanding of block params? Either way, would love to understand this better.

@Samsinite
Copy link

Looks just like what I was seeing in #11277, @stefanpenner @rwjblue any ideas?

@sandstrom
Copy link
Contributor

We're seeing a more or less identical issue in our app.

@sergetab
Copy link

Same problem here.

@stefanpenner
Copy link
Member

Looks just like what I was seeing in #11277, @stefanpenner @rwjblue any ideas?

no need to ping me, i review every notifcation

@Samsinite
Copy link

Sorry I wasn't sure. #11277 seemed to sneak into 1.13 even though I reported 28 days ago.

@typeoneerror
Copy link
Author

@Samsinite I'm positive there are limited hands to review and fix bugs, so my guess is the core team prioritizes fixes. I'm guessing this means that not all issues get fixed in each 6-week release cycle.

If I could make a suggestion, I did notice that your issue was perhaps harder to verify bc you had to clone a whole repo and run a project locally to test. Maybe that slowed the review process?

@Samsinite
Copy link

Possibly, thanks for duplicating the issue into a jsbin. I am guessing that the core team is a little swamped right now when looking at the number of open issues on Github.

Didn't mean to sound ungrateful as the ember core team does awesome work, just wanted them to be aware of the issue.

@typeoneerror
Copy link
Author

@Samsinite no worries. want to close your issue and roll with this one with the jsbin?

@Samsinite
Copy link

Sounds good, this one is newer so it likely has more visibility too.

@ghost
Copy link

ghost commented Jun 24, 2015

I'm having this same issue.

@jmurphyau
Copy link
Contributor

TLDR;

Using the variable being yielded in the component template causes a loop..

Using {{unbound [var]}} and manually calling this.rerender() works around the issue for the time being.


I looked into it and this is what I could find - might help someone with more experience solve this problem..

First Render:

  1. yield constructs the block
  2. RenderResult.build
  3. RenderResult.bindLocals(blockArguments)
  4. [ember-htmlbars] env.bindLocal (for each blockArgument)
  5. stream.notify() (for each blockArgument)

Subsequent Renders (value changes):

  1. yield constructs the block
  2. RenderResult.revalidateWith
  3. RenderResult.updateLocals(blockArguments)
  4. [htmlbars] env.updateLocal (for each blockArgument)
  5. [ember-htmlbars] env.bindLocal (for each blockArgument - updateLocal just calls bindLocal)
  6. stream.notify() (for each blockArgument)

Step 6 on rerender is where the issue starts. This call to notify triggers the component to rerender, which causes it to call yield again and start from Step 1.

Looks like this is because the component is subscribed to this particular stream, and it's subscribed to the stream because it's used in the template body of the component..

Changing the variable used in the component tempalte body to {{unbound [var]}} doesn't cause this issue but doesn't update the template..

Commenting out this line fixes the issue - but I'm not sure what the side effect would be.

Someone with more knowledge on HTMLBars would be able to best come up with a solution I think.

This JSBin is the bare minimum needed to get this issue to occur.
This JSBin uses {{unbound}} and doesn't have the issue but manually calling this.rerender() isn't ideal.. Might get people by for the time being as a work around.

@jmurphyau
Copy link
Contributor

A better workaround is to create a computed property that is used just for yielding that produces the same value.. E.g:

block-with-yield.js

App.BlockWithYieldComponent = Ember.Component.extend({
  danger: 0,
  dangerYield: Ember.computed.oneWay('danger'),
  actions: {
    updateDangerValue: function() {
      this.set('danger', 2);
    }
  }
});

block-with-yield.hbs

<p>
  This text line is inside the component
  [danger = {{danger}}] 
  [<a href='#' {{action 'updateDangerValue'}}>Update Danger Value</a>]
  {{yield dangerYield}}
</p>

JSBin

@tomdale
Copy link
Member

tomdale commented Jun 26, 2015

Looking into this now.

@tomdale
Copy link
Member

tomdale commented Jun 27, 2015

The issue is that Glimmer is re-using the yielded stream from the shadow scope instead of creating a proxy stream. When it re-validates the light scope template, it triggers a change notification on the block param stream. Unfortunately, this is the same stream from the shadow scope, and because danger is yielded in in the example above, it causes an infinite loop of the shadow scope and light scope continuously invalidating one another.

@tomdale
Copy link
Member

tomdale commented Jun 27, 2015

var newValue = Stream.wrap(value, ProxyStream, key);

I believe this line is the culprit. Reading this code, you may assume that the value is always wrapped in a ProxyStream, which is what I believe the original author intended. Unfortunately, that's only true if the value is not a stream (Stream.wrap returns the identity if isStream is true).

@tomdale
Copy link
Member

tomdale commented Jun 27, 2015

I think the call to existing.notify() is unnecessary as well, as @jmurphyau pointed out. In theory we only need to invalidate the stream if the source changes, and indeed, calling setSource on a stream will automatically notify().

The question here is whether we need to create a new ProxyStream for each block param. In theory, we could re-use the value passed in. If it's a stream, we'll get notified when it changes. If it's a primitive, we can wrap it and update it whenever the new primitive comes in.

I can't come up with a reason why we have to force revalidation of the block param stream on re-renders, but maybe I'm forgetting something. It seems like a performance win if we can avoid it.

tomdale added a commit that referenced this issue Jun 27, 2015
This commit removes the forced invalidation of block param streams on
re-renders.

Previously, on re-renders, all block param streams were revalidated by
calling `notify()` on them blindly.

While this often works, it creates a vector for light scopes to
inadvertently invalidate their associated shadow scopes. In some cases,
this leads to an infinite loop.

For example, imagine a component with the following template:

```handlebars
{{! app/templates/components/block-with-yield.hbs }}
{{yield danger}}
```

(Here `danger` is a property on the component.)

Now we invoke it:

```handlebars
{{#block-with-yield as |dangerBlockParam|}}
  {{dangerBlockParam}}
{{/block-with-yield}}
```

This works. The yielded block (light scope) receives a stream for the
`danger` value. On re-renders, the stream is invalidated, but all that
happens is the `{{dangerBlockParam}}` render node is dirtied again
(which doesn’t matter, because we haven’t rendered and cleaned it yet).

However, imagine we change the component template to now display the
same `{{danger}}` value:

```handlebars
{{! app/templates/components/block-with-yield.hbs }}
{{danger}}
{{yield danger}}
```

Now, when the block param stream is invalidated, it goes back and
dirties the render node _from the parent shadow scope_. Because the
component is dirtied, its `{{yield}}` helper is dirtied, causing the
block param streams to be dirtied, and so on in an infinite loop.

This commit removes the forced invalidation, which I believe is
unnecessary and probably left over from an earlier implementation.
Specifically, if we are yielded a stream (`{{yield danger}}`), the
stream should notify us of any changes to the value. Any downstream
consumers of that stream will be notified correctly.

In the case of `{{yield “hello”}}` or some other primitive, we already
wrap those values in a `ProxyStream` and call `setSource` when it
changes, which has the effect of invalidating the stream anyway.

Closes #11519
tomdale added a commit that referenced this issue Jun 28, 2015
This commit removes the forced invalidation of block param streams on
re-renders.

Previously, on re-renders, all block param streams were revalidated by
calling `notify()` on them blindly.

While this often works, it creates a vector for light scopes to
inadvertently invalidate their associated shadow scopes. In some cases,
this leads to an infinite loop.

For example, imagine a component with the following template:

```handlebars
{{! app/templates/components/block-with-yield.hbs }}
{{yield danger}}
```

(Here `danger` is a property on the component.)

Now we invoke it:

```handlebars
{{#block-with-yield as |dangerBlockParam|}}
  {{dangerBlockParam}}
{{/block-with-yield}}
```

This works. The yielded block (light scope) receives a stream for the
`danger` value. On re-renders, the stream is invalidated, but all that
happens is the `{{dangerBlockParam}}` render node is dirtied again
(which doesn’t matter, because we haven’t rendered and cleaned it yet).

However, imagine we change the component template to now display the
same `{{danger}}` value:

```handlebars
{{! app/templates/components/block-with-yield.hbs }}
{{danger}}
{{yield danger}}
```

Now, when the block param stream is invalidated, it goes back and
dirties the render node _from the parent shadow scope_. Because the
component is dirtied, its `{{yield}}` helper is dirtied, causing the
block param streams to be dirtied, and so on in an infinite loop.

This commit removes the forced invalidation, which I believe is
unnecessary and probably left over from an earlier implementation.
Specifically, if we are yielded a stream (`{{yield danger}}`), the
stream should notify us of any changes to the value. Any downstream
consumers of that stream will be notified correctly.

In the case of `{{yield “hello”}}` or some other primitive, we already
wrap those values in a `ProxyStream` and call `setSource` when it
changes, which has the effect of invalidating the stream anyway.

Closes #11519
(cherry picked from commit 2579522)
tomdale added a commit that referenced this issue Jun 28, 2015
This commit removes the forced invalidation of block param streams on
re-renders.

Previously, on re-renders, all block param streams were revalidated by
calling `notify()` on them blindly.

While this often works, it creates a vector for light scopes to
inadvertently invalidate their associated shadow scopes. In some cases,
this leads to an infinite loop.

For example, imagine a component with the following template:

```handlebars
{{! app/templates/components/block-with-yield.hbs }}
{{yield danger}}
```

(Here `danger` is a property on the component.)

Now we invoke it:

```handlebars
{{#block-with-yield as |dangerBlockParam|}}
  {{dangerBlockParam}}
{{/block-with-yield}}
```

This works. The yielded block (light scope) receives a stream for the
`danger` value. On re-renders, the stream is invalidated, but all that
happens is the `{{dangerBlockParam}}` render node is dirtied again
(which doesn’t matter, because we haven’t rendered and cleaned it yet).

However, imagine we change the component template to now display the
same `{{danger}}` value:

```handlebars
{{! app/templates/components/block-with-yield.hbs }}
{{danger}}
{{yield danger}}
```

Now, when the block param stream is invalidated, it goes back and
dirties the render node _from the parent shadow scope_. Because the
component is dirtied, its `{{yield}}` helper is dirtied, causing the
block param streams to be dirtied, and so on in an infinite loop.

This commit removes the forced invalidation, which I believe is
unnecessary and probably left over from an earlier implementation.
Specifically, if we are yielded a stream (`{{yield danger}}`), the
stream should notify us of any changes to the value. Any downstream
consumers of that stream will be notified correctly.

In the case of `{{yield “hello”}}` or some other primitive, we already
wrap those values in a `ProxyStream` and call `setSource` when it
changes, which has the effect of invalidating the stream anyway.

Closes #11519
(cherry picked from commit 2579522)
@Samsinite
Copy link

Rad, thanks guys! This will go in a 1.13.x release before 2.0 right?

@rwjblue
Copy link
Member

rwjblue commented Jun 29, 2015

@Samsinite - Yes, absolutely.

@sandstrom
Copy link
Contributor

I can confirm that this solved our issue! Thanks @jmurphyau and @tomdale

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

No branches or pull requests

9 participants