-
Notifications
You must be signed in to change notification settings - Fork 2k
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
SDK: Try wp.data with calypso state tree #26838
Conversation
8e50377
to
fe25663
Compare
Noting here that I can't boot Calypso on this branch because of that. |
Applied https://github.com/WordPress/gutenberg/pull/9261/files#r212224119 locally. Now failing to boot because of https://github.com/WordPress/gutenberg/blob/a6c36e53974ac8a9fdd6163e61b54064cfd8910f/packages/compose/src/with-safe-timeout/index.js#L19 Edit: Filed WordPress/gutenberg#9265 |
} | ||
|
||
componentDidMount() { | ||
this.updateRegistry( this.props.calypsoStore ); |
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.
It's better to init the registry in the component constructor. Then it will get initialized in server side rendering, too. componentDid{Mount,Update}
is never called in SSR and the registry will remain undefined.
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 we do that, then I'll need to make sure the store
prop never changes. Is that what we want to do?
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.
Also, the calypsoStore
prop doesn't exist when the constructor is run, so it would need to be stitched up afterwards.
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 we do that, then I'll need to make sure the store prop never changes. Is that what we want to do?
The componentDidUpdate
method will still be there to take care of the prop change. I'm suggesting only merging componentDidMount
into the constructor. The constructor is the only one that gets called in both SSR and DOM rendering.
Also, the calypsoStore prop doesn't exist when the constructor is run, so it would need to be stitched up afterwards.
That's strange. Are you sure it doesn't exist? Getting this.props
initialized is the (only) reason why React component constructors often call super( props )
:
constructor( props ) {
// this.props is undefined on this line
super( props );
// the super call just initialized them!
this.registry = this.updateRegistry( this.props.calypsoStore );
}
Is you constructor doing the super
call?
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 just checked again, and calypsoStore
is in fact coming through on the constructor. I'll adjust this to remove componentDidMount
then. Thanks!
calypsoStore.subscribe( () => { | ||
const calypsoState = calypsoStore.getState(); | ||
const hasChanged = calypsoState !== lastCalypsoState; | ||
lastCalypsoState = calypsoState; |
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 assigment could also go into the if
clause below, no?
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, that would be better, thanks!
listeners.push( listener ); | ||
const registryUnsub = registry.subscribe( listener ); | ||
|
||
return () => { |
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.
Maybe add a comment above that we're returning the unsubscribe function here? Or maybe just JSDoc the public methods?
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.
Addressed in 20b3b95
import { getPreference, isFetchingPreferences } from 'state/preferences/selectors'; | ||
|
||
export default { | ||
useCalypsoStore: true, |
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.
Can we drop this bool flag, and just wrap the object in use
, rather than having the consumer do that? I think that would make a bit clearer what we're doing here.
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'm not sure what you mean by this. Can you elaborate or share a code snippet?
|
||
function subscribe( listener ) { | ||
listeners.push( listener ); | ||
const registryUnsub = registry.subscribe( listener ); |
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.
Now that I think about it -- do we even have to take care of passing listeners on to the underlying registry
? It'd be great if we only needed to handle Calypso specific ones...
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 we ever want non-calypso stores to be registered, we'll have to do this.
This seems to be working quite well! For completeness' sake, do we want to add a resolver to this PoC, and remove the query component? |
/** | ||
* Internal dependencies | ||
*/ | ||
import { fetchPreferences, setPreference, savePreference } from 'state/preferences/actions'; |
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 great if we can have our own resolvers, and not rely on any wpcom.undocumented requests.
Looks like we could use apiFetch()
and register our own middleware there for handling WP.com API requests. Has this been considered?
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.
Could be arguably deemed a separate concern:
- Get the adapter to work in current Calypso (this PR)
- Add middleware to adapt to different APIs (depending on whether we're within Calypso, or producing a bundle that's supposed to run on a self-hosted site)
If we're confident that we can solve the latter given the tools that we have, we can tackle it separately.
setPreference, | ||
savePreference, | ||
}, | ||
}; |
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 was also expecting to see a reducer here. Do you think it would make sense to migrate part of the existing reducer to wp.data as well? It would definitely contribute to having a better picture of how a complete example would look.
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.
Making reducers explicit here might help untangle the state tree (relevant for code-splitting?). I guess we'd need to extend the adapter to enable us to attach its collected top-level reducers to Calypso's state tree?
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 agree, but we'll need to add dynamic reducer functionality to the Calypso store as a pre-requisite. I'll start on another PR for that.
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 was also expecting to see a reducer here.
I think it helps to look at it through the "interface vs implementation" lens. The module exports something that adheres to store interface, i.e. it has selectors and action. Reducer is a part of implementation. It's possible to have valid implementations that don't have any reducer at all.
I'd love to get this merged so we can iterate on it and use it for other explorations, but also am a bit hesitant to let it be used in production so early, when we aren't sure that there won't be major changes to the implementation. What do you think about adding it all under a feature flag, and leaving production untouched for now? |
Agree 👍
Hmm, is this worth a feature flag? Code like this https://github.com/Automattic/wp-calypso/pull/26838/files#diff-c5060c1cb159240a551bb8922dbb9743 isn't going to become more readable if we add two different ways of connecting to state. What if we follow a pattern that we've adopted for other SDK related work? Merge the adapter (e.g. the stuff in |
Sounds good to me 👍 as long as we:
|
Before being able to merge this, I think we need to wait for Gutenberg to release package versions that include WordPress/gutenberg#9261 and WordPress/gutenberg#9266 @gziolo, would you do the honors? 😁 |
useCalypsoStore: true, | ||
selectors: { | ||
getPreference, | ||
isFetchingPreferences, |
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.
one of my concerns here is that we'll end up in this file with a giant list of all of our selectors and action creators and this will pin our bundles at full bloat on the initial bundle
do you see a way to avoid this scenario?
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.
can you put them in their own file and import with the asterisk? This is what we do in Gutenberg. We make all selectors and actions public by design.
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.
it's more about the bundle size than the list of imports. if I'm going to use getPreference
in a plugin I don't want to pull in all of the hundreds of selectors, reducers, and action creators from Calypso
also I want this work to benefit Calypso so we can split our data stores there too, so the state code loads via webpack dependencies management
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 we use @wordpress/data
interfaces to access the Calypso Redux store, the interface won't be one giant store with hundreds of actions and selectors. It will be divided into many @wordpress/data
stores: calypso/preferences
, calypso/posts
, calypso/plugins
, ...
The client will access the stores using the select
and dispatch
functions or using the withSelect
and withDispatch
HoCs:
withSelect( select => ( {
pluginsList: select( 'calypso/plugins' ).getWPorgPlugins( { category: 'writing' } )
} ) );
That doesn't necessarily mean that calypso/plugins
will be a separate Redux store. calypso/plugins
is an interface that has some contract (actions and selectors) and the implementation has a lot of freedom on how exactly to fulfill that contract.
It's an open question if these interfaces make code splitting possible or not. I just wanted to point out that the "file with a giant list of all of our selectors" scenario is very unlikely to happen.
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.
maybe I'm mistaken but I believe that the purpose of this PR was to explore a wrapper of all of Calypso's data code behind one store vs. rewriting each separate area of state into its own store, meaning that the purpose was to explore "one giant store with hundreds of actions and selectors."
?
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.
Personally, I'd like to use this as an opportunity to break up the monolith of state we have in Calypso, but I'd like for us to be deliberate about where we draw those lines of separation. I think it could make sense for a collection of selectors to use the same store, if it's similar data. In the end, I'd like to see Calypso using a small number of stores where the division lines are clear. It may also make sense for more than one wp.data
store to use the same state tree within Calypso, for performance reasons (i.e. we don't want to have 50+ redux stores)
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.
In a9a0ebd I moved this to a store named 'calypso/preferences' within a calypso-stores
directory with an index. I think this makes sense and will be much easier to split out from the shared state as well.
I want to do it this week, there are 3 remaining blockers to be resolved as we want to bump most of the packages with major increment:
|
<ReduxProvider store={ this.context.store }> | ||
<CalypsoWPDataProvider calypsoStore={ this.context.store }> | ||
{ content } | ||
</CalypsoWPDataProvider> |
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 this needs to go to client/layout
instead, at least for the purpose of this PR. RootChild
is mostly used by modals, whereas the color scheme picker (like most components) is a direct descendant of client/layout
. I think that in this PR, withSelect()
and withDispatch()
are currently picking up a 'global' registry instance (inside @wp/data
).
I think I got this to work so it picks up the correct registry in my PR, #26930.
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.
Thanks for the tip. I'll adjust this one too.
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 it make sense to have the WP Data Provider in both places? Both here and in ReduxWrappedLayout?
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.
Addressed in e9be45c
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 it make sense to have the WP Data Provider in both places? Both here and in ReduxWrappedLayout?
Eventually, we'll need that Provider at the top of all component hierarchies contain components wrapped in withSelect
and/or withDispatch
, but for the sake of this PR, wrapping only in ReduxWrappedLayout
should be enough.
(We might end up writing a HOC that contains both the Redux and Calypso WP Data Provider, and wrap all component hierarchies in that.)
import { mapValues, without } from 'lodash'; | ||
|
||
const calypsoRegistryPlugin = calypsoStore => registry => { | ||
const namespaces = {}; |
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 fact that the plugin needs to maintain its own registry strikes me as a bit cumbersome. The desired state of things would be that there is one registry that allows to register all types of stores that provide a required interface (i.e., expose selectors
and actions
), no matter what the implementation is.
The current @wordpress/data
registry seems to favor only one particular implementation of a store and doesn't accept any other one.
Using @wordpress/data
public API to select a store now has an unexpected result:
@import { select } from '@wordpress/data';
select( 'calypso' ).colorSchemePreference();
It will crash because the calypso
store is not there: it's in another, private 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 don't recommend using the global select, dispatch, subscribe
exported functions. These are mainly for the WP context and even there we're thinking about "deprecating" them (won't be easy) in favor of always relying on the registry
object (registry.select/dispatch/subcribe
) and the HoCs.
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.
Still, I believe that a single registry should be able to rule them all. Is there any obstacle that would prevent that? I noticed that the P2 integration needs to patch an existing store's resolver (i.e., something that's not public API) -- that would be harder with a registry that doesn't know about store's internals.
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 think a single registry is capable of handing everything. Right now, we use the "default registry" in WordPress packages (and Gutenberg) which may drive Calypso to use the same default registry for now, but I'm hoping we can move away from the default registry in the future and each consumer would have to decide what to include in its registry.
import { createRegistry } from '@wordpress/data';
import coreStoreConfig from '@wordpress/core-data';
import editorStoreConfig from '@wordpress/editor';
import calypsoStoreConfig from '../calypso';
export createRegistry( {
'core': coreStoreConfig,
'core/editor': editorStoreConfig,
'calypso': calypsoStoreConfig,
} );
fe25663
to
20b3b95
Compare
This adds a wp.data plugin that uses the existing calypso state tree but pulls in selectors as a first step to being cross-platform.
This adds action dispatch handling for the wp.data calypso plugin to allow existing Calypso actions to be dispatched against the Calypso store.
This addresses a couple of PR comments about some optimization and additional commenting.
This adds the data provider to ReduxWrappedLayout in addition to RootChild
This moves the preferences selectors/actions from a `calypso` wp.data store to a `calypso/preferences` wp.data store, for the purpose of separating out these stores and organizing them better. This also lays the groundwork for separating out the monolithic state tree in calypso today.
This creates a `custom-store-plugin` which is more general-purpose than the previous `calypso-registry-plugin`. It allows any pre-existing redux store to be used for a given wp.data store instead of wp.data creating one itself. This allows for two things: 1. Stores can use enhancers and middleware, which is not possible in the default wp.data configuration. 2. wp.data stores can share redux stores. Note: This also creates an `internals-plugin`, which is necessary to avoid the reimplementing of internals within the `calypso-registry-plugin`. The `use` function of the `internals-plugin` will be submitted as a PR to core Gutenberg as well so perhaps it will not be necessary for long.
9c06368
to
d7324b4
Compare
I adjusted this PR further to use more general-purpose plugins. I think it makes what is going on here more composable in the future (e.g. specific calypso-based I also rebased this onto the |
This switches the code back to the way it works in @wordpress/data because it's necessary to chain plugins.
This adjusts the plugin to not subscribe to the same custom store more than once when it's passed in as a custom store for more than one wp.data registerStore call.
This is to fix the CI errors on the PR that are complaining about the shrinkwrap not being in sync.
Can this or parts of it move forward, or shall we close this? |
cc @jsnajdr in case you'd like to study this for your wp-data work. |
I'm pretty far detached from this work at this point. However, if someone has questions or wants to discuss anything with me, just let me know and I'll try to remember as best I can. |
This creates a
custom-store-plugin
which is a general-purpose plugin which allows any pre-existing redux store to be used for a given wp.data store instead of wp.data creating one itself. This allows for two things:default wp.data configuration.
Note: This also creates an
internals-plugin
, which is necessary to avoid the reimplementing of internals within thecalypso-registry-plugin
. Theuse
function of theinternals-plugin
will be submitted as a PR to core Gutenberg as well so perhaps it will not be necessary for long.
For review purposes, the
internals-plugin.js
implementation is identical to the@wordpress/data
registry implementation except for theuse
function.To Test:
http://calypso.localhost:3000/me/account
and change the "Admin Color Scheme"withSelect
now.Note: CI Failure is because of a reference to
window
in the@wordpress/data
registry code. I've added a PR to fix this: WordPress/gutenberg#9261