Add withSafeTimeout higher-order component#4839
Conversation
| } | ||
|
|
||
| setSafeTimeout( fn, delay ) { | ||
| this.timeouts.push( setTimeout( fn, delay ) ); |
There was a problem hiding this comment.
Do you think we should remove finished timeouts? Or should we just let them pile up?
There was a problem hiding this comment.
I was picturing more short-lived instances, but good point: we should be defensive anyway.
aduth
left a comment
There was a problem hiding this comment.
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'; |
|
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( … );
}
|
There was a problem hiding this comment.
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.
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.
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.
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
|
There was a problem hiding this comment.
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 ); |
|
|
||
| clearTimeout( id ) { | ||
| clearTimeout( id ); | ||
| this.clear( id ); |
There was a problem hiding this comment.
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.
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.
Maybe simpler (and avoiding unlegible abbreviations 😬 ): https://lodash.com/docs/4.17.4#without
There was a problem hiding this comment.
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? |
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
setTimeoutequivalent.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.copiedafter 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: