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

Framework: Reduxify embeds #38608

Merged
merged 30 commits into from
Feb 5, 2020
Merged

Framework: Reduxify embeds #38608

merged 30 commits into from
Feb 5, 2020

Conversation

tyxla
Copy link
Member

@tyxla tyxla commented Dec 30, 2019

It's been a while since we started migrating all the legacy EventEmitter and flux stores to use redux. 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's ReduceStore. 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:

  • The Redux infrastructure is pretty straightforward and simple
  • A big portion of the added code is tests.
  • Component reduxification goes well together rather than being split into multiple PRs.
  • Commits are atomic and are supposed to provide background to the changes.

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

  • Introduce Redux infrastructure for handling embeds:
    • Action creators
    • Data layer
    • Reducer
    • Utils (used for normalization of embed regex patterns)
    • Selectors
    • Tests for utils, reducer and selectors - not necessary for data layer and action creators as they would be simple dupes of the code that they test.
    • Query component for the embed rendering (QueryEmbed)
  • Reduxify the EmbedView tinymce plugin view component
  • Reduxify the EmbedViewManager component
  • Remove legacy flux embed stores.

Testing instructions

  • Checkout this branch.
  • Go to the post edit screen for a WP.com site.
  • Make sure you're using the Calypso post editor instead of Gutenberg.
  • Try inserting an embeddable URL in your post.
  • Verify it gets converted to a proper embed in visual mode.
  • Compare the embeds behavior with what's in production, it should work the same way.
  • Make sure to test with a clean Redux store.
  • Verify all tests pass.
  • If you'd like to test the Redux infrastructure:
    • Open your browser console.
    • Type dispatch( { type: 'EMBEDS_REQUEST', siteId: 1234 } ) where 1234 is the ID of one of your sites.
    • Verify there's a network request to fetch the supported embed regex patterns.
    • Type state.embeds.siteItems in your browser console.
    • Verify you can see the embed patterns keyed by site ID in the state.
    • Type dispatch( { type: 'EMBED_REQUEST', siteId: 1234, url: 'https://www.facebook.com/20531316728/posts/10154009990506729/' } ) where 1234 is the ID of one of your sites.
    • Verify there's a network request to fetch the embed render data.
    • Type state.embeds.urlItems in your browser console.
    • Verify you can see the embed render data in the state.

Fixes #7924

@tyxla tyxla added [Feature] Post/Page Editor The editor for editing posts and pages. [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. [Type] Task State [Tests] Includes Tests Maintenance labels Dec 30, 2019
@tyxla tyxla requested a review from a team December 30, 2019 09:03
@tyxla tyxla self-assigned this Dec 30, 2019
@matticbot
Copy link
Contributor

@tyxla tyxla mentioned this pull request Dec 30, 2019
4 tasks
@matticbot
Copy link
Contributor

matticbot commented Dec 30, 2019

Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

App Entrypoints (~166767 bytes added 📈 [gzipped])

name                 parsed_size            gzip_size
entry-main             +751609 B  (+84.1%)  +166512 B  (+70.5%)
entry-jetpack-cloud       +795 B   (+0.1%)     +255 B   (+0.1%)

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])

name         parsed_size           gzip_size
post-editor     -13416 B  (-0.7%)    -2957 B  (-0.5%)

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.
Gzip Size: Compressed size of the JS and CSS files. This much data needs to be downloaded over network.

Generated by performance advisor bot at iscalypsofastyet.com.

Copy link
Contributor

@delawski delawski left a 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.

client/state/embeds/test/reducer.js Outdated Show resolved Hide resolved
client/state/selectors/test/get-embed.js Outdated Show resolved Hide resolved
@delawski delawski added [Status] Needs Author Reply and removed [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. labels Dec 31, 2019
@tyxla
Copy link
Member Author

tyxla commented Jan 2, 2020

This could use another review, and I'd appreciate anyone from @Automattic/team-calypso taking a look just in case.

@tyxla tyxla added [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. and removed [Status] Needs Author Reply labels Jan 2, 2020
@tyxla tyxla requested a review from delawski January 2, 2020 14:34
@@ -99,7 +93,7 @@ class EmbedView extends Component {
}

renderFrame() {
if ( ! this.state.wrapper ) {
if ( ! this.state?.wrapper || ! this.props.embed ) {
Copy link
Member

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.

Copy link
Member Author

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 || [];
Copy link
Member

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.

Copy link
Member Author

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';
Copy link
Member

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 ) {

Copy link
Member Author

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?

Copy link
Member

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 👍

Copy link
Member Author

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 => {
Copy link
Member

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.

Copy link
Member Author

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 ) );
Copy link
Member

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.

Copy link
Member Author

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 ) {
Copy link
Member

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.

Copy link
Member Author

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 😉

Copy link
Member Author

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';
Copy link
Member

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.

Copy link
Member Author

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 😉

Copy link
Member Author

Choose a reason for hiding this comment

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

Addressed in #39285.

@tyxla tyxla added [Status] Needs Author Reply and removed [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. labels Jan 15, 2020
@tyxla tyxla added [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. and removed [Status] Needs Author Reply labels Feb 3, 2020
@tyxla
Copy link
Member Author

tyxla commented Feb 3, 2020

@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 🙌

@tyxla tyxla requested a review from jsnajdr February 3, 2020 14:45
Copy link
Member

@jsnajdr jsnajdr left a 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;
}
}
Copy link
Member

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!

Copy link
Member Author

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 ) );
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Makes sense 👍 Created the new selector in 3892ac2 and updated EmbedView to use it in 0417db9

}

removeListener( event, listener ) {
super.removeListener( event, listener );
createListener( store, selector, callback ) {
Copy link
Member

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.

Copy link
Member Author

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? 😉

Copy link
Member

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 🙂

@tyxla
Copy link
Member Author

tyxla commented Feb 5, 2020

Feedback addressed! I think this is ready to go @jsnajdr - wanna give it a 👍 before I 🚢 ? Thank you!

Copy link
Member

@jsnajdr jsnajdr left a 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 👍

@tyxla tyxla merged commit 3a0e741 into master Feb 5, 2020
@tyxla tyxla deleted the reduxify/embeds branch February 5, 2020 18:08
@matticbot matticbot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Feb 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Reduxify: Migrate lib/embeds
4 participants