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: Adding The queried data handling into the entities abstraction #8357

Merged
merged 5 commits into from
Aug 6, 2018

Conversation

youknowriad
Copy link
Contributor

@youknowriad youknowriad commented Aug 1, 2018

This PR supersedes #6395

I wasn't able to rebase it because of the long history, so I just started fresh and copy/pasted code (added a co-author)

Here are the differences with the original PR:

  • This integrates the queries data into the entities abstraction
  • It drops the tracking of the inflight requests in the queried data reducer (we already have the built-in resolvers state in the data module)
  • It drops the constants for consistency with the way we do in all the modules. We can bring them back if we want to expose the module later, I think we'd probably want to expose the entities data entirely and not only the queried-data part.
  • I didn't refactor the existing reducers/selectors. These are meant to be deprecated in Remove unused terms, taxonomies and categories code #8250

Testing instructions

  • Check that the editor loads properly

@youknowriad youknowriad added the [Package] Data /packages/data label Aug 1, 2018
@youknowriad youknowriad self-assigned this Aug 1, 2018
@youknowriad youknowriad force-pushed the add/queries-data branch 3 times, most recently from d095c69 to 0408943 Compare August 1, 2018 13:47
@youknowriad youknowriad requested a review from a team August 2, 2018 10:55
aduth
aduth previously requested changes Aug 2, 2018
Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

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

Noticing a strange regression on this branch. The Tags panel is displayed twice. I cannot manage Categories:

image

@@ -56,13 +61,13 @@ export function addEntities( entities ) {
* @param {string} kind Kind of the received entity.
* @param {string} name Name of the received entity.
* @param {Array|Object} records Records received.
* @param {Object?} query Query Object.
Copy link
Member

Choose a reason for hiding this comment

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

Based on documentation, nullable type should be denoted with ? as prefix, not suffix:

http://usejsdoc.org/tags-type.html

Copy link
Member

Choose a reason for hiding this comment

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

Eslint should be more proactive 😄


/**
* Returns items for a given query, or null if the items are not known. Caches
* result by both per state (by reference) and per query (by deep equality).
Copy link
Member

Choose a reason for hiding this comment

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

Grammar: Remove first "by" in this line.

@@ -148,15 +149,17 @@ export function getEntityRecord( state, kind, name, key ) {
* @param {Object} state State tree
* @param {string} kind Entity kind.
* @param {string} name Entity name.
* @param {?Object} query Optional terms query.
Copy link
Member

Choose a reason for hiding this comment

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

Minor: Alignment on (a) argument name, (b) argument description.

@aduth
Copy link
Member

aduth commented Aug 2, 2018

Likely related to regression: I'm seeing three separate actions dispatched for RECEIVE_ITEMS of the taxonomy type. The last one only includes "Tags". The first has the full set of taxonomies I'd expect.

image

image

image

Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

I left my feedback. This is looking really good. I have some concerns regarding caching strategy. I'm not quite sure what are use cases we are dealing with at the moment. However, I anticipate some issues in the future based on my experience with similar solutions I worked with in the past.

We also should start thinking how to invalidate the cache when data changes on the server and we want re-fetch it because optimistic updates are not sufficient.

*
* @return {Object} Action object.
*/
export function receiveQueriedItems( query = {}, items ) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: It looks like the order of the params should be reversed given that query is optional.

@@ -56,13 +61,13 @@ export function addEntities( entities ) {
* @param {string} kind Kind of the received entity.
* @param {string} name Name of the received entity.
* @param {Array|Object} records Records received.
* @param {Object?} query Query Object.
Copy link
Member

Choose a reason for hiding this comment

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

Eslint should be more proactive 😄

export function getEntityRecords( state, kind, name, query ) {
const queriedState = get( state.entities.data, [ kind, name ] );
if ( ! queriedState ) {
return [];
Copy link
Member

Choose a reason for hiding this comment

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

Should [] be assigned to const, moved out and always return the same reference to avoid rerenders? I think createSelector assured that in the past.

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 this is a rare use-case that can happen only if the entities has just been registered (lazy entities).

post: { slug: 'post' },
page: { slug: 'page' },
},
queries: {
'': [ 'post', 'page' ],
Copy link
Member

Choose a reason for hiding this comment

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

Is this order of items random or can we predict it?

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 it doesn't matter after looking at implementation details.

} );

it( 'should not call reducer if predicate returns false', () => {
const state = {};
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice to wrap with deepFreeze / Object.freeze to ensure there is no mutation.

}

it( 'should call reducer if predicate returns true', () => {
const reducer = createEnhancedReducer( () => true );
Copy link
Member

Choose a reason for hiding this comment

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

Isn't it edge case when state is undefined? There should be another test for an existing state.

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 guess it's not important because the idea is just to check that the reducer was called or not, I'm updating the default state anyway :)

} );

it( 'should ignore actions where property not present', () => {
const state = {};
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice to have initial state wrapped with Object.freeze, too.

switch ( key ) {
case 'page':
case 'perPage':
parts[ key ] = value;
Copy link
Member

Choose a reason for hiding this comment

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

Should value be mapped to string or number to ensure it has the same type when passing to the cache no matter what plugin developer provided as a value? Knowing the dynamic

Copy link
Member

Choose a reason for hiding this comment

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

	it( 'encodes stable string key with page normalized', () => {
		const first = getQueryParts( { '?': '&', b: 2, page: 1 } );
		const second = getQueryParts( { b: 2, '?': '&', page: '1' } );

		expect( first ).toEqual( second );
		expect( first ).toEqual( {
			page: 1,
			perPage: 10,
			stableKey: '%3F=%26&b=2',
		} );
	} );

This fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not certain about this, I'll defer to @aduth as he originally built this utility.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, it seems reasonable to Number( value ) here.

Copy link
Member

Choose a reason for hiding this comment

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

Should value be mapped to string or number to ensure it has the same type when passing to the cache no matter what plugin developer provided as a value?

Normalized to number as of 37e0d7a.

( parts.stableKey ? '&' : '' ) +
encodeURIComponent( key ) +
'=' +
encodeURIComponent( value )
Copy link
Member

Choose a reason for hiding this comment

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

Does it assume that value can by only represented by primitive types?

Otherwise, we should add deep sorting for arrays or objects and serialization mechanism.

Copy link
Member

Choose a reason for hiding this comment

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

Example test case:

	it( 'encodes stable string key when using arrays', () => {
		const first = getQueryParts( { '?': '&', b: [ 3, 2 ] } );
		const second = getQueryParts( { b: [ 2, 3 ], '?': '&' } );

		expect( first ).toEqual( second );
		expect( first ).toEqual( {
			page: 1,
			perPage: 10,
			stableKey: '%3F=%26&b=2',
		} );
	} );

Not sure how stable key should look like in such case.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, currently I think this would only support primitive values. It's not quite so straight-forward, but I could imagine recursing into getQueryParts for non-primitive types.

Copy link
Member

Choose a reason for hiding this comment

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

Does it assume that value can by only represented by primitive types?

Added deep value support (via addQueryArgs) in 0368719.

@youknowriad
Copy link
Contributor Author

Noticing a strange regression on this branch. The Tags panel is displayed twice. I cannot manage Categories:

So the issue was that we were considering a single entity record fetch as a query fetch because we're calling receiveEntityRecords at the end of the resolver with no query argument which was resetting the query items set for the "getEntityRecords" call.

I had two options to fix it:

  • Consider the getEntityRecord call as a separate fetch with no relationship with queried data, which means using another RECEIVE_ITEM action instead of RECEIVE_ITEMS or just use RECEIVE_ITEMS without a query property.

  • Consider getEntityRecord as a special "query". For example when fetching the post with ID: 1, set the query object to { ID: 1 }. That's the option I took in 895463b to fix the issue.

@youknowriad youknowriad dismissed aduth’s stale review August 3, 2018 13:14

It should be fixed now

@aduth
Copy link
Member

aduth commented Aug 3, 2018

or just use RECEIVE_ITEMS without a query property.

On a glance, this is the exact use-case for why receiveItems exists as a separate action creator in the first place.

More to come as I evaluate more deeply..

@aduth
Copy link
Member

aduth commented Aug 3, 2018

I pushed 803b12e as my preferred alternative. I think this could be taken further to a separate receiveEntityRecord action creator if we don't want to have to have receiveEntityRecords vary its logic based on the presence of a query.

@youknowriad
Copy link
Contributor Author

@aduth I'm fine either way, I just thought that the fetch one request is just a "special" query but I don't have strong arguments.

Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

There is one more question that needs to be answered before we proceed: https://github.com/WordPress/gutenberg/pull/8357/files#r207456393. Otherwise, it looks great. Looking forward to seeing it n use 🚀

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.

3 participants