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.
There's a fair bit of inconsistency in how we're creating components here and with relation to conventions of other modules.
withDispatchandwithSelectare in their own files, butwithRegistryis 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-registryon 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-orderfolder.- 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.
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.
even if we separate withRegistry we still need to export two components because
createContextproduces 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.
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.
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.
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.
I think the consumer should inherit the default value if there is no ascendant provider.
There was a problem hiding this comment.
There was a problem hiding this comment.
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.
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.
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.
Actually, it replaces mount as far as I can see. Interesting 🤔
There was a problem hiding this comment.
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.
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 |
| } | ||
| 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.
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.
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.
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.
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.
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.
I think we're saying that we should prefer withSelect over select in most cases.
There was a problem hiding this comment.
But why name the callback argument _select?
There was a problem hiding this comment.
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.
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.
This folder name should be kebab-case, remount-on-prop-change
There was a problem hiding this comment.
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/editornamespace 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.dataacts as the default registry which means the current behavior of the Data Module won't change.RegistryProvider, this is very similar toReduxProvideror any*Providerpattern used in several react libraries.RegistryProviderproviding a custom registry. All calls towithSelect/withDispatchwithin this provider will use the provided registry instead of relying on the globalwp.dataregistry.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/withDispatchunit tests to use theRegistryProviderbut 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