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

Data Module: Introduce the "registry" concept #7527

Merged
merged 3 commits into from
Jul 12, 2018
Merged

Conversation

youknowriad
Copy link
Contributor

Related #7453

Right now, the Data module is acting like a global variable holding a list of registered stores. That's super handy for WordPress Core to allow communication and data access between plugins and Core. But this has the downside of a loss in flexibility. For example:

At the moment, the Gutenberg editor manages its state in the core/editor namespace which makes it very hard to allow embedding multiple Gutenberg editors in the same page because they will shared the same store.

In this PR, we're introducing the concept of a registry to solve this:

  • A registry is a collection of stores
  • The current wp.data acts as the default registry which means the current behavior of the Data Module won't change.
  • You can create custom registries and wrap parts of the application in a RegistryProvider, this is very similar to ReduxProvider or any *Provider pattern used in several react libraries.
  • When you wrap a component (or a tree of components) in a RegistryProvider providing a custom registry. All calls to withSelect/withDispatch within this provider will use the provided registry instead of relying on the global wp.data registry.

Notes

Testing instructions

  • Just make sure the editor loads like today.

@youknowriad youknowriad added the [Package] Data /packages/data label Jun 25, 2018
@youknowriad youknowriad self-assigned this Jun 25, 2018
@youknowriad youknowriad requested review from aduth and a team June 25, 2018 10:05
@youknowriad
Copy link
Contributor Author

mmm, it looks like even our current tests are failing because of Enzyme. I wonder how we should move forward with this: Rewrite all these tests using react-test-rendered?

@youknowriad
Copy link
Contributor Author

I updated the tests to use react-test-renderer. It should be fine now.

@@ -0,0 +1,26 @@
/**
Copy link
Member

Choose a reason for hiding this comment

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

There's a fair bit of inconsistency in how we're creating components here and with relation to conventions of other modules.

  1. withDispatch and withSelect are in their own files, but withRegistry is combined into the provider file
    • If we want a pattern of component-per-file, we should be strictly consistent. Case in point: I searched with-registry on this page expecting to find the file where this component was defined, but it did not exist.
  2. Other modules we have folder pattern components/registry-provider/index.js .
    • We may argue this is the "next step of simplest", but if we're already to the point of splitting by component, we may as well strive for consistency. It also provides an easier pattern for ensuring we're responsible for including README per component.
  3. Other modules we place higher-order components within a nested higher-order folder.
    • While the naming/placement could be subject to debate, the point is: (a) consistency, (b) accurately reflecting that these aren't actually components in their own right, and can't be used directly, only as an enhancer to another component.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair points:

I'm leaning towards:

components/registry-provider/index.js / components/with-select/index.js. I personally don't think the higher-order folder is necessary because sometimes we need both from the same file createContext calls.

I can extract with-registry but my idea was to avoid exporting the Consumer entirely. It's minor though. but even if we separate withRegistry we still need to export two components because createContext produces two components. So not certain how to go with this.

Copy link
Member

Choose a reason for hiding this comment

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

even if we separate withRegistry we still need to export two components because createContext produces two components. So not certain how to go with this.

I think it's fine to have an exception for createContext, mostly out of necessity.

return ( props ) => (
<Consumer>
{ ( registry ) => (
<OriginalComponent registry={ registry } { ...props } />
Copy link
Member

Choose a reason for hiding this comment

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

We'll have the same problem I encountered in #7453, which is that withDispatch and withSelect pass through all props they receive. And for some components this is problematic because they then spread onto the DOM elements (e.g. Button), thus producing warnings about unrecognized attributes.

To preempt the obvious solution to use omit( this.props, [ 'registry' ] ), I prefer my solution in #7453 which is marginally more performant: pass the props unaffected by a separate ownProps prop. This also has the effect of not including registry in mapSelectToProps / mapDispatchToProps.

@gziolo
Copy link
Member

gziolo commented Jun 25, 2018

Just noting, that I very like the fact that we are moving code to their own files. The benefit of this is that we no longer need to mock the function that registers store because it is no longer autoloaded.

aduth
aduth previously requested changes Jun 26, 2018
*/
import { createContext } from '@wordpress/element';

const { Consumer, Provider } = createContext( null );
Copy link
Member

Choose a reason for hiding this comment

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

Rather than having withDispatch and withSelect know about the default registry, could we just assign it as the default here instead of null? Avoids need for special handling, where:

const dispatch = this.props.registry ? this.props.registry.dispatch : defaultRegistry.dispatch;

Becomes:

const { dispatch } = this.props.registry;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the issue is that people use withSelect/ withDispatch without using a provider. I'm wondering if the default value is stored in the provider or can be provided by the consumer even without any provider. I'll have to check.

Copy link
Member

Choose a reason for hiding this comment

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

I think the consumer should inherit the default value if there is no ascendant provider.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

I think the consumer should inherit the default value if there is no ascendant provider.

We use this behavior both in tests and for Disabled component (see #7138). This is why you have to provide this default value when creating context.

Copy link
Member

Choose a reason for hiding this comment

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

If I follow properly, the following should do the trick:

const { Consumer, Provider } = createContext( createRegistry() );

};
} )( ( props ) => <button onClick={ props.increment } /> );

const testRenderer = TestRenderer.create(
Copy link
Member

@gziolo gziolo Jun 27, 2018

Choose a reason for hiding this comment

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

My own notes: to replace shallow from enzyme we would need only:

jest.shallow = ( element ) => {
    const TestRenderer  = require( 'react-test-renderer' );
    const testRenderer = TestRenderer.create( element );
    const testInstance = testRenderer.root;
    if ( testInstance !== null) { // not sure if it can be null?
        testInstance.update = testRenderer.update;
    }
    return testInstance;
}; 

Not that much of the work needed :)

Copy link
Member

Choose a reason for hiding this comment

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

Actually, it replaces mount as far as I can see. Interesting 🤔

Copy link
Member

Choose a reason for hiding this comment

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

Would be good to explore removing enzyme in lieu of this approach in a separate PR. The lack of React 16 support is frustrating. My only concern is whether or not it makes our tests run significantly slower.

Copy link
Member

@gziolo gziolo Jun 27, 2018

Choose a reason for hiding this comment

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

Yes, definitely something we should explore further. Actually, enzyme brings lots of performance penalty on its own. See:
https://github.com/WordPress/packages/blob/master/packages/jest-preset-default/scripts/setup-test-framework.js#L3-L6
We lazy load it only when it is necessary to mitigate that :)
It isn't as much of an issue at the moment, but I could observe a huge difference when dealing with 1 000+ test suites in Calypso.

@atimmer
Copy link
Member

atimmer commented Jul 6, 2018

Looks good. I've tested it with our custom store and it all still works.

I had one question: Does this mean that the data in the core store will now potentially be duplicated between registries, or will we always refer to the wp.data store if we need core data?

@aduth
Copy link
Member

aduth commented Jul 11, 2018

I took it upon myself to rebase this one, since I'm largely to blame for the conflicts. Wasn't the easiest to rebase, but I think I managed to pull it off in 77f19ca I did it in a separate branch just in case something's wrong with it, otherwise you can rebase and cherry-pick into this one.

One remaining point of feedback I have yet is: In withSelect, with RegistryConsumer passing the registry as a prop, do we need to account for the fact that registry could change? i.e. do we need to unsubscribe and resubscribe to the new registry?

@noisysocks
Copy link
Member

One remaining point of feedback I have yet is: In withSelect, with RegistryConsumer passing the registry as a prop, do we need to account for the fact that registry could change? i.e. do we need to unsubscribe and resubscribe to the new registry?

I imagine it's very rare that one would dynamically change the registry fed into RegistryProvider. If it's easy to support, then let's do it. If it's not, then maybe a console.warn would suffice?

Copy link
Member

@noisysocks noisysocks left a comment

Choose a reason for hiding this comment

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

Love this refactor! I'm not seeing any regressions when I test locally 👍

Left a few comments—nothing blocking.

}
export { default as withSelect } from './components/with-select';
export { default as withDispatch } from './components/with-dispatch';
export { default as RegistryProvider } from './components/registry-provider';
Copy link
Member

Choose a reason for hiding this comment

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

Should we export createRegistry from ./registry so that third party developers can create their own registries?

}

Object.entries( {
'core/data': dataStore,
Copy link
Member

Choose a reason for hiding this comment

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

Is it right that every registry created has a core/data store? Or should we instead make only the default registry have this? e.g.

// ./registry.js:
Object.entries( storeConfig ).map( ( [ name, config ] ) => registerStore( name, config ) );

// ./default-registry.js:
import dataStore from './store';
export default createRegistry( { 'core/data': dataStore } );

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Every registry should have a dataStore IMO to keep track of resolvers resolution.

describe( 'withDispatch', () => {
let registry;
beforeEach( () => {
registry = createRegistry();
Copy link
Member

Choose a reason for hiding this comment

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

I love how this approach makes these unit tests not rely on any global state 😍

// wp.data export. But in-fact, this serves as a good deterrent for
// including both `withSelect` and `select` in the same scope, which
// shouldn't occur for a typical component, and if it did might wrongly
// encourage the developer to use `select` within the component itself.
Copy link
Member

Choose a reason for hiding this comment

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

Bit confused by this. Are we saying that all users of withSelect should name the first callback argument _select? Why say this in a unit test and not in the README?

We already have a lint rule in Gutenberg which would prevent the select in import { select } from '@wordpress/data' from accidentally being shadowed by withSelect.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we're saying that we should prefer withSelect over select in most cases.

Copy link
Member

@noisysocks noisysocks Jul 12, 2018

Choose a reason for hiding this comment

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

But why name the callback argument _select?

Copy link
Member

Choose a reason for hiding this comment

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

The idea is that we discourage select in components by virtue of the shadowing lint rule.

This code is bad and will be made obviously bad to the developer by lint warning between select the import and select the argument:

import { select, withSelect } from '@wordpress/data';

class Foo extends Component {
	myFunction() {
		// ...
		select( 'core/editor' ).getSomething();
	}
}

export default withSelect( ( select ) => {
	// ...
} )( Foo );

In the tests, we don't care (because we're testing both select and withSelect), which is why we're okay to do the ugly aliasing. The comment is meant to explain this, but apparently does a poor job of doing so.

Copy link
Member

Choose a reason for hiding this comment

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

That makes sense! It hadn't occurred to me that we're testing both select and withSelect in these tests 🤦‍♂️

@youknowriad
Copy link
Contributor Author

youknowriad commented Jul 12, 2018

I might need some help rebasing this @aduth I don't want to mess up with the latest changes :)

Edit: Sorry, I missed the comment above, it should be fine.

@youknowriad youknowriad force-pushed the try/data-registry branch 2 times, most recently from 57cb9ce to 775119b Compare July 12, 2018 12:58
@aduth
Copy link
Member

aduth commented Jul 12, 2018

One remaining point of feedback I have yet is: In withSelect, with RegistryConsumer passing the registry as a prop, do we need to account for the fact that registry could change? i.e. do we need to unsubscribe and resubscribe to the new registry?

I imagine it's very rare that one would dynamically change the registry fed into RegistryProvider. If it's easy to support, then let's do it. If it's not, then maybe a console.warn would suffice?

I wonder if we could use some key trickery to force the unmount when the registry changes, like what we do in RichText for TinyMCE:

// Generating a key that includes `tagName` ensures that if the tag
// changes, we unmount and destroy the previous TinyMCE element, then
// mount and initialize a new child element in its place.
const key = [ 'editor', Tagname ].join();

@youknowriad
Copy link
Contributor Author

I updated with the remountOnPropChange HoC which remounts the withSelect/withDispatch when the registry prop changes. It should be ready to go

@youknowriad youknowriad merged commit 5fb24e4 into master Jul 12, 2018
@@ -0,0 +1,42 @@
/**
Copy link
Member

Choose a reason for hiding this comment

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

This folder name should be kebab-case, remount-on-prop-change

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, I just noticed it too. This will be fixed in the @wordpress/compose PR I'm working on

@gziolo
Copy link
Member

gziolo commented Jul 12, 2018

Should we document new behavior and method?

@mtias mtias added this to the 3.3 milestone Jul 20, 2018
@youknowriad youknowriad deleted the try/data-registry branch January 23, 2019 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Data /packages/data
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants