-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Framework: Reduxify embeds #38608
Framework: Reduxify embeds #38608
Conversation
Test live: https://calypso.live/?branch=reduxify/embeds |
Here is how your PR affects size of JS and CSS bundles shipped to the user's browser: App Entrypoints (~166767 bytes added 📈 [gzipped])
Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used. Sections (~2957 bytes removed 📉 [gzipped])
Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to. Legend What is parsed and gzip size?Parsed Size: Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory. Generated by performance advisor bot at iscalypsofastyet.com. |
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.
@tyxla Thank you working on this!
I've tested both visually and in console and embeds seem to work as expected.
I've also left some comments regarding the tests. Please let me know what do you think about the suggestions.
While the Flux to Redux migration details make sense to me, I'm not quite familiar with Flux so it's hard for me to do a thorough review of this piece. In order to get some more orientation, I've read the docs about our approach to data (nb: I've updated some links there - please review this little PR when you get a chance: #38623), but I think that some experience with the technology is needed here first of all.
The PR looks good to me and I'm approving it now with an expectation that you'll have a second look into the test issues I mentioned in the code.
This could use another review, and I'd appreciate anyone from @Automattic/team-calypso taking a look just in case. |
@@ -99,7 +93,7 @@ class EmbedView extends Component { | |||
} | |||
|
|||
renderFrame() { | |||
if ( ! this.state.wrapper ) { | |||
if ( ! this.state?.wrapper || ! this.props.embed ) { |
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.
It's better to set the initial state to { wrapper: null }
and avoid the optional chaining with state?.wrapper
.
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.
Good call, done in 10736af748c98669da281919246395762be1fec1.
* @param {object} response API response | ||
* @returns {Array} Array of embeds. | ||
*/ | ||
const fromApi = response => response.embeds || []; |
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.
Personally, I would avoid creating this fromApi
handler, and would move the logic to the EMBEDS_RECEIVE
reducer and the normalizeEmbeds
function.
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 see why you suggest this, but I think I prefer it as-is. The idea here is that we're separating the API endpoint specifics (the fact that the endpoint puts it into a .embeds
field) from the actual normalization logic that expects just an array of embed patterns. I like keeping those two separate, and I think that fromApi
helps us isolate those concerns elegantly.
* Internal dependencies | ||
*/ | ||
import { EMBED_RECEIVE, EMBED_REQUEST, EMBEDS_RECEIVE, EMBEDS_REQUEST } from 'state/action-types'; | ||
import 'state/data-layer/wpcom/sites/embeds'; |
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.
After the data layer migration, we can also remove this wpcom.undocumented
function that's no longer used:
UndocumentedSite.prototype.embeds = function( attributes, 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.
Oh, it seems this one is still used in EmbedDialog
. I'd be happy to remove that in a subsequent PR, as this one has grown quite a bit. Does that sound good to you?
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, I didn't notice the other usage. Let's not snowball this PR by trying to remove it 👍
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've decided not to do this for now. The way EmbedDialog
is architectured right now relies too much on the internals of how wpcom
performs requests. To decouple from that and rely on the data layer + traditional redux query component and selector, we'll have to refactor the entire component, which I'd rather strive away from at this time.
|
||
this.sitesListener = this.updateSite.bind( this ); | ||
EmbedViewManager.match = content => { |
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.
This new implementation of match
has two flaws:
First, it dispatches requestEmbeds
on each call, even if the embed list is already loaded. The original implementation didn't do that. That means that the /embeds
REST request is sent very frequently as the user edits the content.
Second, if on the first call to match
the embeds list is not yet loaded, it doesn't find any of the embeds. And when the list load is finished, we need to somehow trigger the match
again. The original implementation achieved that by being an EventEmitter
instance and becoming part of the emitters
list. The new implementation doesn't subscribe to changes in the Redux store at all.
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.
Thanks, really good catch.
I was actually able to repro the issue you're talking about (the second one) by delaying the initial embeds request to take longer. Unfortunately, it seems like the TinyMCE plugins rely on the emitters quite a bit. This is something we can't really get away from in this PR. It's likely a worthy effort, but not for this PR.
So, I've reverted the EmbedViewManager
to an EventEmitter
, but changed it so it now subscribes to changes of interest in the Redux store. That way, match
will be called again whenever the embed patterns are loaded, and the embeds will be properly rendered.
Furthermore, I've modified it so we'll only request site embed patterns once.
Let me know what you think.
actions.fetch( this.siteId ); | ||
} | ||
} | ||
reduxDispatch( requestEmbed( siteId, url ) ); |
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.
The request to render the embed should be a part of the EmbedView
component, using the classical query+selector pattern:
function EmbedView() {
return (
<div>
<QueryEmbed siteId={ ... } url={ ... } />
{ renderFrame() }
</div>
);
}
connect( state => ( {
embed: getEmbed( state, siteId, url )
} ) );
The match
function only needs the list of the site's embed regexps, and is not concerned with embed rendering at all.
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.
Good call, moved the actual embed render request to be handled by a query component that lives in EmbedView
.
@@ -81,7 +73,7 @@ class EmbedView extends Component { | |||
} | |||
|
|||
setHtml() { | |||
if ( ! this.state.body || ! this.refs.iframe ) { | |||
if ( ! this.props.embed?.body || ! this.refs.iframe ) { |
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.
It shouldn't be hard to convert the refs from strings to modern React.createRef
.
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.
Sure 👍 I'd prefer doing that in a follow-up PR though, I hope you don't mind? This one is too big anyway, so I try hard to refrain from any non-critical changes 😉
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.
Addressing this in #39316
import { getSelectedSiteId } from 'state/ui/selectors'; | ||
import { SELECTED_SITE_SUBSCRIBE, SELECTED_SITE_UNSUBSCRIBE } from 'state/action-types'; |
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.
After this PR, the SELECTED_SITE_SUBSCRIBE
actions can be removes, as their only usage is gone. You'll still need to subscribe to the Redux store, but that shouldn't be done with store.subscribe()
. Layers of indirection with actions and middleware are not needed.
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.
Like the other cleanups, I'm happy to address it in another PR to keep this one as small as possible. It's far from being small already 😉
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.
Addressed in #39285.
98f1880
to
1cccda7
Compare
@jsnajdr thanks for the thorough review, nice catches. I've fixed the critical problems you've outlined, while suggesting several follow-up PRs. If that sounds good, I'll follow-up with them in PRs once this one gets merged. They are:
This is ready for another 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.
Nice! I'm posting a few suggestions on how to improve the code without changing behavior.
render() { | ||
return null; | ||
} | ||
} |
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.
To write query components, hooks and the useDispatch
hook is an awesome tool:
function QueryEmbeds( { siteId, url } ) {
const dispatch = useDispatch();
useEffect( () => {
dispatch( requestEmbed( siteId, url ) );
}, [ dispatch, siteId, url ] );
return null;
}
And that's the whole component!
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 like that, thanks - much simpler 👍
Implemented in d264692
this.updateSite(); | ||
} | ||
getCurrentSiteEmbeds( state ) { | ||
return getSiteEmbeds( state, getSelectedSiteId( state ) ); |
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.
getCurrentSiteEmbeds
doesn't need to be a method. It doesn't access this
. It can be a standalone function -- a Redux selector like any other.
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.
} | ||
|
||
removeListener( event, listener ) { | ||
super.removeListener( event, listener ); | ||
createListener( store, selector, 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.
createListener
also doesn't need to be a method. It would be also nice if I could pass an array of selectors:
createListener( store, [ getSelectedSiteId, getCurrentSiteEmbeds ], callback );
That's somewhat similar to how hooks handle dependency arrays.
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 think I'd prefer the explicitness of the current way it's written. I hope you wouldn't mind if I keep it as-is? 😉
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.
OK, but I still think it doesn't need to be a method 🙂
b401429
to
f6a1096
Compare
Feedback addressed! I think this is ready to go @jsnajdr - wanna give it a 👍 before I 🚢 ? Thank you! |
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.
Still looks good, thanks for the great work here 👍
It's been a while since we started migrating all the legacy
EventEmitter
andflux
stores to useredux
. Most of them have been migrated, but there are still some remaining, mostly big and thorny ones.This PR creates the embeds Redux infrastructure, and ports the existing embed flux stores usage to Redux. Finally, it removes those legacy flux stores.
This removes the last usages of
flux/utils
and of flux'sReduceStore
. A thorough list of changes can be found in the list below.I'm happy to split it into multiple PRs, but I didn't because:
Note that I didn't create a query component for the list of embed patterns, simply because it wasn't needed.
Changes proposed in this Pull Request
QueryEmbed
)EmbedView
tinymce plugin view componentEmbedViewManager
componentTesting instructions
dispatch( { type: 'EMBEDS_REQUEST', siteId: 1234 } )
where1234
is the ID of one of your sites.state.embeds.siteItems
in your browser console.dispatch( { type: 'EMBED_REQUEST', siteId: 1234, url: 'https://www.facebook.com/20531316728/posts/10154009990506729/' } )
where1234
is the ID of one of your sites.state.embeds.urlItems
in your browser console.Fixes #7924