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

Gutenberg: make sure that editor initialization is executed only once #28302

Merged
merged 3 commits into from
Nov 6, 2018

Conversation

vindl
Copy link
Member

@vindl vindl commented Nov 6, 2018

Fixes #27882 and #27993

Changes proposed in this Pull Request

The problem with our current editor implementation is that whenever we navigate away from it (by clicking Close) and then navigate back, all of the initialization logic will be executed again. That means that API middleware will be applied twice, core blocks registration will be attempted again (producing warnings) etc.

I discussed this with @Automattic/team-calypso in #27882 and @jsnajdr proposed two options:

  1. placing the init in the default export function of client/gutenberg/editor/index.js.
  2. placing the init in the post controller wrapped with lodash/once.

After experimenting a bit with both approaches, I decided to go with option 2. It seems more readable and easier to reason about to me, because all of the setup logic and sequencing is in one place. We also don't have to pull in redux-bridge for it.

Here is the summary of what has been moved to new initialization function:

  • All of the init code from main.jsx - This includes: data plugins registration, core-data init, API middleware setup, Classic Block init, Core Blocks registration, and TinyMCE plugins init.
  • The API middleware has been refactored and the utility component has been removed. Previously, this component was ensuring that API middleware was applied before the editor starts firing API requests. However, it was not accounting for the above issue, so same middleware was applied multiple times. I suspect this didn't surface earlier because we are breaking the middleware chain - if that was not the case our mapping would be broken after navigating away and back.
  • I moved the editor extension loading (presets) to init function too, since it seems like a better place to contain that functionality. Previously it was placed in edit-post folder, and we'd like to avoid adding Calypso's custom modifications there as much as possible. Maintaining it is hard enough with frequent core changes, and adding our custom code there only makes it harder. We are working to move away from it as soon as possible, but it's not an immediate priority.

Testing instructions

  • localStorage.setItem( 'debug', 'calypso:gutenberg' );
  • Navigate to http://calypso.localhost:3000/gutenberg/post/
  • Notice the editor initialization debug messages in console.
  • Click Close.
  • Click Back in your browser (at this point there is a similar visual glitch that we observed in Calypsoify redirects, not related to this PR and present in wpcalypso too).
  • Verify that editor initialization has not been executed twice by observing the console log. Currently in wpcalypso you'd see repeated core blocks registration warnings.
  • Smoke test the editor and verify that this is not causing any regressions.

@vindl vindl added [Type] Bug When a feature is broken and / or not performing as intended [Type] Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. [Goal] Gutenberg Working towards full integration with Gutenberg labels Nov 6, 2018
@matticbot
Copy link
Contributor

@vindl vindl self-assigned this Nov 6, 2018
@vindl vindl requested a review from a team November 6, 2018 07:28
@vindl vindl force-pushed the update/gutenberg-initialize-once branch from 90eb78e to 19a65aa Compare November 6, 2018 07:32
@vindl vindl requested review from a team, sirreal and simison November 6, 2018 07:43
@sirreal sirreal requested review from a team and removed request for simison and sirreal November 6, 2018 09:21
vindl added 2 commits November 6, 2018 15:20
Previously the editor initialization fucntions were executed whenever
we navigated away and returned back from it. This resulted in unwanted
consequences like trying to register the same blocks twice and applying
the same middleware two times.
Now that we have an initialization function that is guaranteed to
execute only once, we can move the API middleware initialization
into it too.
@vindl vindl force-pushed the update/gutenberg-initialize-once branch from 84d87c7 to ed44754 Compare November 6, 2018 14:22
We want to avoid putting any Calypso custom code into edit-post folder.
New editor initialization function seems like a better place to do it.
@vindl vindl force-pushed the update/gutenberg-initialize-once branch from ed44754 to b328877 Compare November 6, 2018 14:28
@vindl vindl merged commit 9e77736 into master Nov 6, 2018
@vindl vindl deleted the update/gutenberg-initialize-once branch November 6, 2018 16:56
@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 Nov 6, 2018

// We need to ensure that his function is executed only once to avoid duplicate
// block registration, API middleware application etc.
export const initGutenberg = once( ( userId, siteSlug ) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

once is not applied unique per argument set. The function will be called once ever.

How then do we ensure applyAPIMiddleware( siteSlug ) is called if I edit a post for a second site after I'd already initialized it once before for a first site?

Instead you might want to use a memoization utility capable of handling multiple argument. Lodash's does not. Gutenberg uses (my) memize to this effect.

⇒ n_      
n_ > const initGutenberg = ( userId, siteSlug ) => console.log( 'called with', userId, siteSlug );
undefined
n_ > const initGutenbergWithOnce = _.once( initGutenberg );
undefined
n_ > initGutenbergWithOnce( 1, 'example1.wordpress.com' )
called with 1 example1.wordpress.com
undefined
n_ > initGutenbergWithOnce( 1, 'example2.wordpress.com' )
undefined
n_ > const initGutenbergWithLodashMemoize = _.memoize( initGutenberg );
undefined
n_ > initGutenbergWithLodashMemoize( 1, 'example1.wordpress.com' )
called with 1 example1.wordpress.com
undefined
n_ > initGutenbergWithLodashMemoize( 1, 'example2.wordpress.com' )
undefined
n_ > const initGutenbergWithMemize = require( 'memize' )( initGutenberg );
undefined
n_ > initGutenbergWithMemize( 1, 'example1.wordpress.com' )
called with 1 example1.wordpress.com
undefined
n_ > initGutenbergWithMemize( 1, 'example2.wordpress.com' )
called with 1 example2.wordpress.com
undefined

Or, if the rest of the code is not meant to / cannot be run, save for the API middleware application, maybe it ought to be pulled out and applied separately.

Copy link
Member

Choose a reason for hiding this comment

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

The initGutenberg function is a mix of initialization tasks we really want to do just once per app lifetime (like various requires and module inits) and other tasks we want to do per site or even per editor instance.

The siteSlug here is used to rewrite the URLs of API requests and point them to the per-site WP.com endpoints. once means that these request will be made to the first site visited, forever, even after site switch. We can rewrite that to a callback function:

() => getSelectedSiteSlug( store.getState() );

Then each request will use the the correct site ID, but will still write the results to the singleton core/data store, mixing results from different sites. Not exactly an improvement. Memoizing the init function better won't help either, as long as the data stores remain global singletons.

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 catch @aduth! I haven't considered that while refactoring the initialization.

Or, if the rest of the code is not meant to / cannot be run, save for the API middleware application, maybe it ought to be pulled out and applied separately.

I think it's fine to run the rest only once - for example, we don't want to attempt to register core blocks twice (would generate warnings) etc. So it looks like we should only pull out API middleware application.

Copy link
Member Author

Choose a reason for hiding this comment

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

Then each request will use the the correct site ID, but will still write the results to the singleton core/data store, mixing results from different sites. Not exactly an improvement. Memoizing the init function better won't help either, as long as the data stores remain global singletons.

@jsnajdr I'm not sure how to resolve this, given the way core data stores and packages are currently working. Do you think that extending this PR by @Copons to reinitialize the editor on each site switch could work? - #29310

Copy link
Member

@jsnajdr jsnajdr Dec 12, 2018

Choose a reason for hiding this comment

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

I think the combination of the following two changes should work:

  1. Reset the core store on each site switch. core is currently not among the resettable stores in Gutenlypso: Reset part of the Gutenberg stores when navigating to the editor #29310 and needs reset only on site switch, not on each editor load.
  2. Instead of passing a constant siteSlug to the wpcomPathMappingMiddleware, pass a getSiteSlug callback that returns the latest siteSlug on every call. That makes the middleware map to a different path on a site switch.

If you end up wpcomPathMappingMiddleware, I propose the following API change: instead of this:

wpcomPathMappingMiddleware = ( options, next, getSiteSlug ) => ...
use( partialRight( wpcomPathMappingMiddleware, getSiteSlug ) );

do this:

wpcomPathMappingMiddleware = getSiteSlug => ( options, next ) => ...
use( wpcomPathMappingMiddleware( getSiteSlug ) );

(if you agree it looks like an improvement)

Copy link
Member Author

Choose a reason for hiding this comment

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

Instead of passing a constant siteSlug to the wpcomPathMappingMiddleware, pass a getSiteSlug callback that returns the latest siteSlug on every call. That makes the middleware map to a different path on a site switch.

Landed this in #29368. I'm going to look into point 1 next.

Copy link
Member Author

Choose a reason for hiding this comment

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

The first part is resolved in #29445 by @Copons.

Copy link
Contributor

Choose a reason for hiding this comment

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

should be 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Goal] Gutenberg Working towards full integration with Gutenberg [Type] Bug When a feature is broken and / or not performing as intended [Type] Enhancement Changes to an existing feature — removing, adding, or changing parts of it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants