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

Declarative Reflexes Resiliency #606

Merged

Conversation

marcoroth
Copy link
Member

@marcoroth marcoroth commented Sep 1, 2022

Type of PR

Improvement

Description

This Pull Requests fixes two problems:
a) the double-fire of reflexes when invoking the reflex once
b) StimulusReflex not executing the client-side Stimulus controller callbacks when invoking a reflex

Because of load order reasons (which are mainly just apparent in Import Maps powered apps) setupDeclarativeReflexes() isn't always run at right time. In browsers, where the Import Maps are not natively supported yet, it leads to the fact that the setupDeclarativeReflexes() is ran twice. Since there is also no guarantee of load order we can't assume that all Stimulus controller instances are loaded yet.

In some instances this had weird side effects. If you have this element:

<button 
  data-reflex="click->Example#create" 
  data-controller="example"
>Create</button>

it would convert the element to this the after calling setupDeclarativeReflexes() the first time around, because the example controller instance is not setup yet:

<button 
  data-reflex="click->Example#create" 
- data-controller="example"
+ data-controller="example stimulus-reflex" 
+ data-action="click->stimulus-reflex#__perform"
>Create</button>

and after calling setupDeclarativeReflexes() the second time with the example controller now available:

<button 
  data-reflex="click->Example#create" 
  data-controller="example stimulus-reflex" 
- data-action="click->stimulus-reflex#__perform"
+ data-action="click->stimulus-reflex#__perform click->example#__perform"
>Create</button>

The fact why this issue appeared in the first place is because the setupDeclarativeReflexes() is looking if the developer defined a data-controller on the element, and if so we are using that controller so we can invoke the callbacks in that controller.

But at the time when the setupDeclarativeReflexes() function is being run the first time the example controller instance isn't connected yet and our function then opts for setting the generic stimulus-reflex controller instead. At the second time the example controller instance is loaded and it will append the second action descriptor, because that one wasn't present already so it causes the action descriptor to be set twice causing the double-fire issue.

Because there are now two action descriptors setup on that element it's also going to invoke the reflex twice. This Pull Requests refactors the setupDeclarativeReflexes() function and makes it aware of previously set action descriptors so it can replace them instead of just appending them to prevent the double-fire issue.

I guess depending on the browser used and the browser version it would not add the action descriptor twice, but instead would just use the wrong Stimulus controller and therefore not invoke the Stimulus controller callbacks.

In order to fix that, we extract a function setupDeclarativeReflexesForElement(element) which can be called with a single element to setup the declarative reflexes on it. This function is being called when the Stimulus controller calls StimulusReflex.register(this), which then setups the right action descriptors, guaranteeing that the right controller is actually present.

Because of this approach, we might even be able to remove the global event listener for setting up the declarative reflexes on document:

document.addEventListener('readystatechange', () => {
if (document.readyState === 'complete') {
setupDeclarativeReflexes()
}
})

Fixes #605
Follow-up of #602

Why should this be added

Ensures that the data-reflex attribute gets properly converted to it's corresponding data-controller and data-action attributes by making the conversion independent of the events dispatched on window or document, but rather by hooking into the StimulusReflex.register(this) function which anyway just gets called when the Stimulus controller gets connected, meaning that the right Stimulus controller instance will always be available.

Checklist

  • My code follows the style guidelines of this project
  • Checks (StandardRB & Prettier-Standard) are passing
  • This is not a documentation update

javascript/attributes.js Outdated Show resolved Hide resolved
@marcoroth marcoroth added bug Something isn't working enhancement New feature or request javascript Pull requests that update Javascript code labels Sep 1, 2022
@marcoroth marcoroth added this to the 3.5 milestone Sep 1, 2022
Copy link
Contributor

@leastbad leastbad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally like how you've approached this, and agree that it's smart to have register call a per-element version of this function.

Are you confident that filtering controllers on !action.includes("#__perform") will properly handle scenarios where a single element could have Reflex actions declared from multiple classes? For example input->foo#__perform click->bar#__perform.

If we get rid of the global setupDeclarativeReflexes, we need another place to call emitEvent('stimulus-reflex:ready'). Indeed, it's not clear that we even confidently know that more controllers won't load in.

@marcoroth
Copy link
Member Author

Are you confident that filtering controllers on !action.includes("#__perform") will properly handle scenarios where a single element could have Reflex actions declared from multiple classes? For example input->foo#__perform click->bar#__perform.

Yeah, that should be covered, it might be, that we are invoking the function twice for such an element, but that shouldn't matter now since it should be resilient enough.

@marcoroth
Copy link
Member Author

We might be able to emit the StimulusReflex ready event after the initialize function finshed or after the ActionCable connection established, since we don't need to "care" about the controllers in that case

@leastbad
Copy link
Contributor

leastbad commented Sep 1, 2022

We might be able to emit the StimulusReflex ready event after the initialize function finshed or after the ActionCable connection established, since we don't need to "care" about the controllers in that case

The reason that event gets emitted is to tell the developer that it's ready to process events. If we emit before the controllers have registered themselves, then the event will come before the handlers are installed. This was fine in a syncronous config...

Otherwise, if you're confident about multiple classes working fine (should we add a test for this?) LGTM.

@marcoroth
Copy link
Member Author

Okay I kept the global event listener with the setupDeclarativeReflexes() so that we still have a stimulus-reflex:ready being emitted with the best guess we have available.

I also added some tests for setupDeclarativeReflexesForElement().

@marcoroth marcoroth force-pushed the declarative-reflexes-resiliency branch from cc18bea to 348c9dd Compare September 4, 2022 06:58
@marcoroth marcoroth mentioned this pull request Sep 4, 2022
@leastbad
Copy link
Contributor

leastbad commented Sep 6, 2022

Working through this, I'm now of the opinion that stimulus-reflex:ready should only be called once per hard refresh, but I don't think that this is the correct place to enforce this.

@leastbad leastbad merged commit 500e6c9 into stimulusreflex:master Sep 6, 2022
@marcoroth marcoroth deleted the declarative-reflexes-resiliency branch September 6, 2022 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working enhancement New feature or request javascript Pull requests that update Javascript code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Reflex action happens twice when clicking once on a button with a Stimulus controller
2 participants