Skip to content

Commit 26062c8

Browse files
alshakeroepiqueras
authored andcommitted
Core-data: do not publish outdated state to subscribers during updates (#19752)
* Core-data: do not publish outdated state to subscribers during updates Calling `saveEntityRecord` with an update does the following: 1. Calls `getEntityRecord` to fetch the current persisted state of the entity record 2. Calls `receiveEntityRecords` with the new up-to-date state to render the updates 3. Sends an API fetch with the update patch to persist the update 4. Calls `receiveEntityRecords` again with the new up-to-date *persisted* state The issue here, is that point 1 (Calling `getEntityRecord`) not only fetches the persisted state, but it also internally calls `receiveEntityRecords` itself . This results in the persisted outdated server state to be rendered on the UI, causing a flickering effect, that jumps pack when point 2 takes turn. This commit removes the call to getEntityRecord, and instead, it just calls receiveEntityRecords with the local up-to-date state of the entity record. This fixes the flickering issue. * Core-data: update tests to match saveEntityRecord yeilded actions Given `saveEntityRecord` no longer selects `getEntityRecord`, which itself triggers a SELECT action, two SELECTs are no longer yielded. This commit removes the expectation of these two SELECTs. * Core-data: Introduce getEntityRecordNoResolver selector To allow saveEntityRecord access the latest local full version of an entity without issung an API request. This prevents propogating outdating states to subscribers when saveEntityRecord is called. See: #19752 (comment) * Address review comments at #19752: 1. Capitalize alll added comment messages 2. Alias getEntityRecord with getEntityRecordNoResolver instead of copying it 3. Use describe.each instaed of looping manually in selectors tests
1 parent 24c0e60 commit 26062c8

File tree

6 files changed

+61
-8
lines changed

6 files changed

+61
-8
lines changed

docs/designers-developers/developers/data/data-core.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,21 @@ _Returns_
202202

203203
- `?Object`: The entity record's non transient edits.
204204

205+
<a name="getEntityRecordNoResolver" href="#getEntityRecordNoResolver">#</a> **getEntityRecordNoResolver**
206+
207+
Returns the Entity's record object by key. Doesn't trigger a resolver nor requests the entity from the API if the entity record isn't available in the local state.
208+
209+
_Parameters_
210+
211+
- _state_ `Object`: State tree
212+
- _kind_ `string`: Entity kind.
213+
- _name_ `string`: Entity name.
214+
- _key_ `number`: Record's key
215+
216+
_Returns_
217+
218+
- `?Object`: Record.
219+
205220
<a name="getEntityRecords" href="#getEntityRecords">#</a> **getEntityRecords**
206221

207222
Returns the Entity's records.

packages/core-data/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,21 @@ _Returns_
415415

416416
- `?Object`: The entity record's non transient edits.
417417

418+
<a name="getEntityRecordNoResolver" href="#getEntityRecordNoResolver">#</a> **getEntityRecordNoResolver**
419+
420+
Returns the Entity's record object by key. Doesn't trigger a resolver nor requests the entity from the API if the entity record isn't available in the local state.
421+
422+
_Parameters_
423+
424+
- _state_ `Object`: State tree
425+
- _kind_ `string`: Entity kind.
426+
- _name_ `string`: Entity name.
427+
- _key_ `number`: Record's key
428+
429+
_Returns_
430+
431+
- `?Object`: Record.
432+
418433
<a name="getEntityRecords" href="#getEntityRecords">#</a> **getEntityRecords**
419434

420435
Returns the Entity's records.

packages/core-data/src/actions.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,11 +357,10 @@ export function* saveEntityRecord(
357357
}
358358
}
359359

360-
// We perform an optimistic update here to clear all the edits that
361-
// will be persisted so that if the server filters them, the new
362-
// filtered values are always accepted.
360+
// Get the full local version of the record before the update,
361+
// to merge it with the edits and then propagate it to subscribers
363362
persistedEntity = yield select(
364-
'getEntityRecord',
363+
'getEntityRecordNoResolver',
365364
kind,
366365
name,
367366
recordId

packages/core-data/src/selectors.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,20 @@ export function getEntityRecord( state, kind, name, key ) {
107107
return get( state.entities.data, [ kind, name, 'queriedData', 'items', key ] );
108108
}
109109

110+
/**
111+
* Returns the Entity's record object by key. Doesn't trigger a resolver nor requests the entity from the API if the entity record isn't available in the local state.
112+
*
113+
* @param {Object} state State tree
114+
* @param {string} kind Entity kind.
115+
* @param {string} name Entity name.
116+
* @param {number} key Record's key
117+
*
118+
* @return {Object?} Record.
119+
*/
120+
export function getEntityRecordNoResolver( state, kind, name, key ) {
121+
return getEntityRecord( state, kind, name, key );
122+
}
123+
110124
/**
111125
* Returns the entity's record object by key,
112126
* with its attributes mapped to their raw values.

packages/core-data/src/test/actions.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ describe( 'saveEntityRecord', () => {
4141
expect( fulfillment.next( entities ).value.type ).toBe(
4242
'SAVE_ENTITY_RECORD_START'
4343
);
44+
45+
// Should select getEntityRecordNoResolver selector (as opposed to getEntityRecord)
46+
// see https://github.com/WordPress/gutenberg/pull/19752#discussion_r368498318.
4447
expect( fulfillment.next().value.type ).toBe( 'SELECT' );
45-
expect( fulfillment.next().value.type ).toBe( 'SELECT' );
48+
expect( fulfillment.next().value.selectorName ).toBe( 'getEntityRecordNoResolver' );
4649
expect( fulfillment.next().value.type ).toBe( 'SELECT' );
4750
expect( fulfillment.next().value.type ).toBe( 'RECEIVE_ITEMS' );
4851
const { value: apiFetchAction } = fulfillment.next( {} );

packages/core-data/src/test/selectors.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import deepFreeze from 'deep-freeze';
88
*/
99
import {
1010
getEntityRecord,
11+
getEntityRecordNoResolver,
1112
getEntityRecords,
1213
getEntityRecordChangesByRecord,
1314
getEntityRecordNonTransientEdits,
@@ -20,7 +21,11 @@ import {
2021
getReferenceByDistinctEdits,
2122
} from '../selectors';
2223

23-
describe( 'getEntityRecord', () => {
24+
// getEntityRecord and getEntityRecordNoResolver selectors share the same tests
25+
describe.each( [
26+
[ getEntityRecord ],
27+
[ getEntityRecordNoResolver ],
28+
] )( '%p', ( selector ) => {
2429
it( 'should return undefined for unknown record’s key', () => {
2530
const state = deepFreeze( {
2631
entities: {
@@ -36,7 +41,7 @@ describe( 'getEntityRecord', () => {
3641
},
3742
},
3843
} );
39-
expect( getEntityRecord( state, 'root', 'postType', 'post' ) ).toBe( undefined );
44+
expect( selector( state, 'root', 'postType', 'post' ) ).toBe( undefined );
4045
} );
4146

4247
it( 'should return a record by key', () => {
@@ -56,7 +61,9 @@ describe( 'getEntityRecord', () => {
5661
},
5762
},
5863
} );
59-
expect( getEntityRecord( state, 'root', 'postType', 'post' ) ).toEqual( { slug: 'post' } );
64+
expect( selector( state, 'root', 'postType', 'post' ) ).toEqual( {
65+
slug: 'post',
66+
} );
6067
} );
6168
} );
6269

0 commit comments

Comments
 (0)