-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
Conversation
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 |
I updated the tests to use |
@@ -0,0 +1,26 @@ | |||
/** |
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.
There's a fair bit of inconsistency in how we're creating components here and with relation to conventions of other modules.
withDispatch
andwithSelect
are in their own files, butwithRegistry
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.
- If we want a pattern of component-per-file, we should be strictly consistent. Case in point: I searched
- 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.
- 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.
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.
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.
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.
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 } /> |
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.
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
.
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. |
*/ | ||
import { createContext } from '@wordpress/element'; | ||
|
||
const { Consumer, Provider } = createContext( 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.
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;
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 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.
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 the consumer should inherit the default value if there is no ascendant provider.
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.
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 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.
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.
If I follow properly, the following should do the trick:
const { Consumer, Provider } = createContext( createRegistry() );
}; | ||
} )( ( props ) => <button onClick={ props.increment } /> ); | ||
|
||
const testRenderer = TestRenderer.create( |
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.
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 :)
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.
Actually, it replaces mount
as far as I can see. Interesting 🤔
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.
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.
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.
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.
39f4a9f
to
0e46b9a
Compare
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 |
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 |
I imagine it's very rare that one would dynamically change the |
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.
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'; |
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.
Should we export createRegistry
from ./registry
so that third party developers can create their own registries?
} | ||
|
||
Object.entries( { | ||
'core/data': dataStore, |
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.
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 } );
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.
Every registry should have a dataStore IMO to keep track of resolvers resolution.
describe( 'withDispatch', () => { | ||
let registry; | ||
beforeEach( () => { | ||
registry = createRegistry(); |
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 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. |
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.
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
.
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 we're saying that we should prefer withSelect
over select
in most cases.
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.
But why name the callback argument _select
?
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 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.
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.
That makes sense! It hadn't occurred to me that we're testing both select
and withSelect
in these tests 🤦♂️
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. |
57cb9ce
to
775119b
Compare
I wonder if we could use some gutenberg/editor/components/rich-text/index.js Lines 859 to 862 in 81983b7
|
775119b
to
80376c6
Compare
I updated with the |
@@ -0,0 +1,42 @@ | |||
/** |
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 folder name should be kebab-case, remount-on-prop-change
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.
yes, I just noticed it too. This will be fixed in the @wordpress/compose
PR I'm working on
Should we document new behavior and method? |
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:
wp.data
acts as the default registry which means the current behavior of the Data Module won't change.RegistryProvider
, this is very similar toReduxProvider
or any*Provider
pattern used in several react libraries.RegistryProvider
providing a custom registry. All calls towithSelect
/withDispatch
within this provider will use the provided registry instead of relying on the globalwp.data
registry.Notes
To achieve WIP: Blocks: Reimplement shared block as embedded editor #7453 we need a way to create a registry from another registry and replace one of its stores. I'm intentionally not including such an API in this PR and leaving it for WIP: Blocks: Reimplement shared block as embedded editor #7453 when we'll actually need it.
I planned to update the
withSelect
/withDispatch
unit tests to use theRegistryProvider
but since Enzyme doesn't support React context yet, I'm holding off. It's not crucial though because we already test the global registry andcreateRegistry
.Testing instructions