-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Add withSafeTimeout higher-order component #4839
Conversation
} | ||
|
||
setSafeTimeout( fn, delay ) { | ||
this.timeouts.push( setTimeout( fn, delay ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think we should remove finished timeouts? Or should we just let them pile up?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was picturing more short-lived instances, but good point: we should be defensive anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool 👍 Agree with @iseulde's point.
Related but not necessary here: Might be worth exploring ESLint rules restricting non-HOC use of timers in components, or even also warning about safe usage (i.e. still expecting them to be used in some cases, but discouraging them as a crutch)
/** | ||
* WordPress dependencies | ||
*/ | ||
import { Component, withSafeTimeout } from '@wordpress/components'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Component
is unused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hawk eye!
Agree on limiting usage of this and guarding against |
At first I thought the Do you think it should be called |
Been wondering this too. Pro of Let's try
What do you mean? Expand as in wrap the function, or expand as in accommodate its friends |
Yeah in this case I'm leaning towards |
One issue I might see is that it introduces variable shadowing with the global const { setTimeout } = this.props;
// Later...
setTimeout( ... ); The latter point could be perceived as a benefit, since the behavior should be identical anyways to the global, but there is magic involved all the same. |
That's true, though for me it highlights an equivalent but more common problem, which is identifier collision between an action creator and its bound counterpart: import { doThing } from 'actions'
connect(
null,
{ doThing }
)( ( { doThing } ) => ( // oops, eslint go sad
<button onClick={ doThing } />
) ) Gutenberg has, for the most part, avoided this by using its weird action naming convention: connect( null, { onDoThing: doThing } )( … ) which is a whole other topic and which I personally don't like (in this example, I'd either go for render() {
const { foos } = this.props;
return <button onClick={ this.props.doThing }>{ foos }</button>;
} In Gutenberg, where functional components are more common, I could see this emerge: import { getFoos } from 'selectors'
import { doThing } from 'actions'
connect(
( state ) => ( { foos: getFoos( state ) } ),
{ doThing }
)( ( { foos, ...actions } ) => (
<button onClick={ actions.doThing }>{ foos }</button>
) ) The point of this detour is that I'd welcome a convention that would have us refer to effectful functions with some sort of namespacing ( |
While I'd be lying if I said the convention wasn't at all related to the shadowing issue, I don't entirely agree in it being "weird" if considering the implied distinction between presentational and container components. I'd agree though that this is often forced and at best still a pain point in props naming.
One practical concern I might have here can be seen in how this transpiles via Babel: Note the iteration. Obviously I'd not be fond of letting the specifics of the transpile dictate how code is best written, but it can be a factor, or at least worth noting. |
Thanks for checking, it's useful for weighing the discussion.
I agree with this, and appreciate the decoupling in general. My criticism is targeted at this pattern: connect( null, { onMultiSelect: multiSelect } )
// later, in the component:
expandSelection( … ) {
…
this.props.onMultiSelect( … );
}
|
return ( | ||
<OriginalComponent | ||
{ ...this.props } | ||
setTimeout={ this.setTimeout } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we return the timeoutID
, in case someone wants to clear the timeout not just on unmount? Should we pass clearTimeout
as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we return the
timeoutID
Absolutely, which I just noticed I didn't do.
Pass
clearTimeout
This I'm not so sure. What would we provide? The same as window.clearTimeout
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. It might be more convenient to get it from the same place. Without it I'd need to get one from props, and "import" the other from the window. Maybe it's also more future-proof.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I guess to be super correct, it would also need to call this.clear( id )
.
Pushed The behavior is illustrated by the following screencast, where I intentionally added a couple of seconds to the
|
|
||
clear( id ) { | ||
const newTimeouts = this.timeouts.filter( t => t !== id ); | ||
this.timeouts = newTimeouts; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason not to shorten this to the following?
this.timeouts = this.timeouts.filter( t => t !== id );
4500940
to
ff410da
Compare
Oh, no offense taken at all. I'd agree with you, and your example of |
b3edc98
to
360a51d
Compare
That sounds good, though I can't do it now (have to step out for the evening). I can take care of it tomorrow, or feel free to add those. |
Yes. I wonder if |
blocks/rich-text/patterns.js
Outdated
* @param {Function} callback The function to call. | ||
*/ | ||
function setSafeTimeout( callback ) { | ||
console.log( 'setSafeTimeout', callback ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove console.log
?
|
||
clearTimeout( id ) { | ||
clearTimeout( id ); | ||
this.clear( id ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically I think it'd be fine if the logic of clear
was included here, than done separately, since clearTimeout
on a finished timeout should be a noop (the reason I'm guessing you separated clear
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, the instinct was the separation, but we can merge.
} | ||
|
||
clear( id ) { | ||
this.timeouts = this.timeouts.filter( t => t !== id ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe simpler (and avoiding unlegible abbreviations 😬 ): https://lodash.com/docs/4.17.4#without
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heh, yes. A consequence of quick prototyping.
Yes. I wonder if editor.removed is still needed... Theoretically it is only removed on unmount, and then the timer is cleared.
Yes, sounds about right. Wanted to check with you. :)
|
e571c35
to
8728a64
Compare
Feedback addressed; final review? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonderful :)
8728a64
to
cd735cb
Compare
Stumbled upon some logic in embeds, wondering if we should offer a similarly bound version of gutenberg/blocks/library/embed/index.js Lines 86 to 89 in e539ea0
|
Description
Sparked by recent in situ conversations and by #4603, here's an attempt to make asynchronous function calling safer using a lifecycle-bound
setTimeout
equivalent.Depending on use cases, I could also see this accommodating more functions (
setInterval
, perhaps evendebounce
), whether via HoC arguments or separate HoCs.How Has This Been Tested?
CopyContentButton
's behavior.copied
.copied
after that.editor/hooks/copy-content/index.js
, replace thesetSafeTimeout( … )
call with the standardwindow.setTimeout
. Repeat step 5 and make sure that the message gets logged even though the menu was previously closed.Screenshots (jpeg or gifs if applicable):
Types of changes
Checklist: