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

SDK: Try wp.data with calypso state tree #26838

Closed

Conversation

coderkevin
Copy link
Contributor

@coderkevin coderkevin commented Aug 23, 2018

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:

  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.

For review purposes, the internals-plugin.js implementation is identical to the @wordpress/data registry implementation except for the use function.

To Test:

  1. Start calypso in local dev mode as usual.
  2. Go to http://calypso.localhost:3000/me/account and change the "Admin Color Scheme"
  3. Behavior should be the same, but it's using 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

@coderkevin coderkevin self-assigned this Aug 23, 2018
@matticbot
Copy link
Contributor

@coderkevin coderkevin force-pushed the try/wp-data-calypso-plugin branch 2 times, most recently from 8e50377 to fe25663 Compare August 23, 2018 05:10
@coderkevin coderkevin changed the title Data: Try wp.data with calypso state tree SDK: Try wp.data with calypso state tree Aug 23, 2018
@ockham
Copy link
Contributor

ockham commented Aug 23, 2018

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

Noting here that I can't boot Calypso on this branch because of that.

}

componentDidMount() {
this.updateRegistry( this.props.calypsoStore );
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

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.

Copy link
Member

@jsnajdr jsnajdr Aug 23, 2018

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?

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 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;
Copy link
Contributor

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?

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, that would be better, thanks!

listeners.push( listener );
const registryUnsub = registry.subscribe( listener );

return () => {
Copy link
Contributor

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?

Copy link
Contributor Author

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,
Copy link
Contributor

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.

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'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 );
Copy link
Contributor

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...

Copy link
Contributor Author

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.

@ockham
Copy link
Contributor

ockham commented Aug 23, 2018

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';
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 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?

Copy link
Contributor

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:

  1. Get the adapter to work in current Calypso (this PR)
  2. 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,
},
};
Copy link
Member

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.

Copy link
Contributor

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?

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 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.

Copy link
Member

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.

@tyxla
Copy link
Member

tyxla commented Aug 24, 2018

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?

@ockham
Copy link
Contributor

ockham commented Aug 24, 2018

I'd love to get this merged so we can iterate on it and use it for other explorations,

Agree 👍

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?

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 client/state/wp-data/, but not the modified components, but rather use the adapter solely in client/gutenberg/extensions/ for now?

@tyxla
Copy link
Member

tyxla commented Aug 24, 2018

What if we follow a pattern that we've adopted for other SDK related work? Merge the adapter (e.g. the stuff in client/state/wp-data/, but not the modified components, but rather use the adapter solely in client/gutenberg/extensions/ for now?

Sounds good to me 👍 as long as we:

  • Don't rely on the adapter for anything in production
  • Can use this work for further explorations and iterate on it

@alisterscott alisterscott added [Project] SDK [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. labels Aug 27, 2018
@ockham
Copy link
Contributor

ockham commented Aug 27, 2018

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,
Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

@dmsnell dmsnell Aug 27, 2018

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

Copy link
Member

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.

Copy link
Member

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."

?

Copy link
Contributor Author

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)

Copy link
Contributor Author

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.

@gziolo
Copy link
Member

gziolo commented Aug 27, 2018

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

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>
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in e9be45c

Copy link
Contributor

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 = {};
Copy link
Member

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.

Copy link
Contributor

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 registryobject (registry.select/dispatch/subcribe) and the HoCs.

Copy link
Member

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.

Copy link
Contributor

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,
} ); 

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.
@coderkevin coderkevin changed the base branch from master to update/gutenberg-packages September 6, 2018 07:16
@coderkevin
Copy link
Contributor Author

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 wp.data stores can be moved away from the monolithic Calypso store one at a time. This should also support resolvers at this point as well.

I also rebased this onto the update/gutenberg-packages branch until that PR gets merged. This fixes the problem of the window global for SSR.

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.
@sirreal
Copy link
Member

sirreal commented Nov 27, 2020

Can this or parts of it move forward, or shall we close this?

@simison simison closed this Nov 27, 2020
@simison simison deleted the try/wp-data-calypso-plugin branch November 27, 2020 11:14
@simison
Copy link
Member

simison commented Nov 27, 2020

cc @jsnajdr in case you'd like to study this for your wp-data work.

@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 27, 2020
@coderkevin
Copy link
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.