@wordpress/data 6.8.0 → 6.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/README.md +28 -0
- package/build/components/use-select/index.js +113 -0
- package/build/components/use-select/index.js.map +1 -1
- package/build/index.js +22 -3
- package/build/index.js.map +1 -1
- package/build/redux-store/index.js +52 -5
- package/build/redux-store/index.js.map +1 -1
- package/build/registry.js +31 -4
- package/build/registry.js.map +1 -1
- package/build-module/components/use-select/index.js +111 -0
- package/build-module/components/use-select/index.js.map +1 -1
- package/build-module/index.js +13 -1
- package/build-module/index.js.map +1 -1
- package/build-module/redux-store/index.js +52 -5
- package/build-module/redux-store/index.js.map +1 -1
- package/build-module/registry.js +31 -4
- package/build-module/registry.js.map +1 -1
- package/package.json +8 -8
- package/src/components/use-select/index.js +127 -0
- package/src/components/use-select/test/index.js +71 -0
- package/src/components/use-select/test/suspense.js +160 -0
- package/src/index.js +16 -1
- package/src/redux-store/index.js +43 -0
- package/src/registry.js +30 -4
- package/tsconfig.json +2 -2
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { render, waitFor } from '@testing-library/react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WordPress dependencies
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
createRegistry,
|
|
11
|
+
createReduxStore,
|
|
12
|
+
useSuspenseSelect,
|
|
13
|
+
RegistryProvider,
|
|
14
|
+
} from '@wordpress/data';
|
|
15
|
+
import { Component, Suspense } from '@wordpress/element';
|
|
16
|
+
|
|
17
|
+
jest.useRealTimers();
|
|
18
|
+
|
|
19
|
+
function createRegistryWithStore() {
|
|
20
|
+
const initialState = {
|
|
21
|
+
prefix: 'pre-',
|
|
22
|
+
token: null,
|
|
23
|
+
data: null,
|
|
24
|
+
fails: true,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const reducer = ( state = initialState, action ) => {
|
|
28
|
+
switch ( action.type ) {
|
|
29
|
+
case 'RECEIVE_TOKEN':
|
|
30
|
+
return { ...state, token: action.token };
|
|
31
|
+
case 'RECEIVE_DATA':
|
|
32
|
+
return { ...state, data: action.data };
|
|
33
|
+
default:
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const selectors = {
|
|
39
|
+
getPrefix: ( state ) => state.prefix,
|
|
40
|
+
getToken: ( state ) => state.token,
|
|
41
|
+
getData: ( state, token ) => {
|
|
42
|
+
if ( ! token ) {
|
|
43
|
+
throw 'missing token in selector';
|
|
44
|
+
}
|
|
45
|
+
return state.data;
|
|
46
|
+
},
|
|
47
|
+
getThatFails: ( state ) => state.fails,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const sleep = ( ms ) => new Promise( ( r ) => setTimeout( () => r(), ms ) );
|
|
51
|
+
|
|
52
|
+
const resolvers = {
|
|
53
|
+
getToken: () => async ( { dispatch } ) => {
|
|
54
|
+
await sleep( 10 );
|
|
55
|
+
dispatch( { type: 'RECEIVE_TOKEN', token: 'token' } );
|
|
56
|
+
},
|
|
57
|
+
getData: ( token ) => async ( { dispatch } ) => {
|
|
58
|
+
await sleep( 10 );
|
|
59
|
+
if ( ! token ) {
|
|
60
|
+
throw 'missing token in resolver';
|
|
61
|
+
}
|
|
62
|
+
dispatch( { type: 'RECEIVE_DATA', data: 'therealdata' } );
|
|
63
|
+
},
|
|
64
|
+
getThatFails: () => async () => {
|
|
65
|
+
await sleep( 10 );
|
|
66
|
+
throw 'resolution failed';
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const store = createReduxStore( 'test', {
|
|
71
|
+
reducer,
|
|
72
|
+
selectors,
|
|
73
|
+
resolvers,
|
|
74
|
+
} );
|
|
75
|
+
|
|
76
|
+
const registry = createRegistry();
|
|
77
|
+
registry.register( store );
|
|
78
|
+
|
|
79
|
+
return { registry, store };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe( 'useSuspenseSelect', () => {
|
|
83
|
+
it( 'renders after suspending a few times', async () => {
|
|
84
|
+
const { registry, store } = createRegistryWithStore();
|
|
85
|
+
let attempts = 0;
|
|
86
|
+
let renders = 0;
|
|
87
|
+
|
|
88
|
+
const UI = () => {
|
|
89
|
+
attempts++;
|
|
90
|
+
const { result } = useSuspenseSelect( ( select ) => {
|
|
91
|
+
const prefix = select( store ).getPrefix();
|
|
92
|
+
const token = select( store ).getToken();
|
|
93
|
+
const data = select( store ).getData( token );
|
|
94
|
+
return { result: prefix + data };
|
|
95
|
+
}, [] );
|
|
96
|
+
renders++;
|
|
97
|
+
return <div aria-label="loaded">{ result }</div>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const App = () => (
|
|
101
|
+
<RegistryProvider value={ registry }>
|
|
102
|
+
<Suspense fallback="loading">
|
|
103
|
+
<UI />
|
|
104
|
+
</Suspense>
|
|
105
|
+
</RegistryProvider>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const rendered = render( <App /> );
|
|
109
|
+
await waitFor( () => rendered.getByLabelText( 'loaded' ) );
|
|
110
|
+
|
|
111
|
+
// Verify there were 3 attempts to render. Suspended twice because of
|
|
112
|
+
// `getToken` and `getData` selectors not being resolved, and then finally
|
|
113
|
+
// rendered after all data got loaded.
|
|
114
|
+
expect( attempts ).toBe( 3 );
|
|
115
|
+
expect( renders ).toBe( 1 );
|
|
116
|
+
} );
|
|
117
|
+
|
|
118
|
+
it( 'shows error when resolution fails', async () => {
|
|
119
|
+
const { registry, store } = createRegistryWithStore();
|
|
120
|
+
|
|
121
|
+
const UI = () => {
|
|
122
|
+
const { token } = useSuspenseSelect( ( select ) => {
|
|
123
|
+
// Call a selector whose resolution fails. The `useSuspenseSelect`
|
|
124
|
+
// is then supposed to throw the resolution error.
|
|
125
|
+
return { token: select( store ).getThatFails() };
|
|
126
|
+
}, [] );
|
|
127
|
+
return <div aria-label="loaded">{ token }</div>;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
class Error extends Component {
|
|
131
|
+
state = { error: null };
|
|
132
|
+
|
|
133
|
+
static getDerivedStateFromError( error ) {
|
|
134
|
+
return { error };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
render() {
|
|
138
|
+
if ( this.state.error ) {
|
|
139
|
+
return <div aria-label="error">{ this.state.error }</div>;
|
|
140
|
+
}
|
|
141
|
+
return this.props.children;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const App = () => (
|
|
146
|
+
<RegistryProvider value={ registry }>
|
|
147
|
+
<Error>
|
|
148
|
+
<Suspense fallback="loading">
|
|
149
|
+
<UI />
|
|
150
|
+
</Suspense>
|
|
151
|
+
</Error>
|
|
152
|
+
</RegistryProvider>
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const rendered = render( <App /> );
|
|
156
|
+
const label = await waitFor( () => rendered.getByLabelText( 'error' ) );
|
|
157
|
+
expect( label.textContent ).toBe( 'resolution failed' );
|
|
158
|
+
expect( console ).toHaveErrored();
|
|
159
|
+
} );
|
|
160
|
+
} );
|
package/src/index.js
CHANGED
|
@@ -19,7 +19,10 @@ export {
|
|
|
19
19
|
RegistryConsumer,
|
|
20
20
|
useRegistry,
|
|
21
21
|
} from './components/registry-provider';
|
|
22
|
-
export {
|
|
22
|
+
export {
|
|
23
|
+
default as useSelect,
|
|
24
|
+
useSuspenseSelect,
|
|
25
|
+
} from './components/use-select';
|
|
23
26
|
export { useDispatch } from './components/use-dispatch';
|
|
24
27
|
export { AsyncModeProvider } from './components/async-mode-provider';
|
|
25
28
|
export { createRegistry } from './registry';
|
|
@@ -115,6 +118,18 @@ export const select = defaultRegistry.select;
|
|
|
115
118
|
*/
|
|
116
119
|
export const resolveSelect = defaultRegistry.resolveSelect;
|
|
117
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Given the name of a registered store, returns an object containing the store's
|
|
123
|
+
* selectors pre-bound to state so that you only need to supply additional arguments,
|
|
124
|
+
* and modified so that they throw promises in case the selector is not resolved yet.
|
|
125
|
+
*
|
|
126
|
+
* @param {string|StoreDescriptor} storeNameOrDescriptor Unique namespace identifier for the store
|
|
127
|
+
* or the store descriptor.
|
|
128
|
+
*
|
|
129
|
+
* @return {Object} Object containing the store's suspense-wrapped selectors.
|
|
130
|
+
*/
|
|
131
|
+
export const suspendSelect = defaultRegistry.suspendSelect;
|
|
132
|
+
|
|
118
133
|
/**
|
|
119
134
|
* Given the name of a registered store, returns an object of the store's action creators.
|
|
120
135
|
* Calling an action creator will cause it to be dispatched, updating the state value accordingly.
|
package/src/redux-store/index.js
CHANGED
|
@@ -162,10 +162,12 @@ export default function createReduxStore( key, options ) {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
const resolveSelectors = mapResolveSelectors( selectors, store );
|
|
165
|
+
const suspendSelectors = mapSuspendSelectors( selectors, store );
|
|
165
166
|
|
|
166
167
|
const getSelectors = () => selectors;
|
|
167
168
|
const getActions = () => actions;
|
|
168
169
|
const getResolveSelectors = () => resolveSelectors;
|
|
170
|
+
const getSuspendSelectors = () => suspendSelectors;
|
|
169
171
|
|
|
170
172
|
// We have some modules monkey-patching the store object
|
|
171
173
|
// It's wrong to do so but until we refactor all of our effects to controls
|
|
@@ -200,6 +202,7 @@ export default function createReduxStore( key, options ) {
|
|
|
200
202
|
resolvers,
|
|
201
203
|
getSelectors,
|
|
202
204
|
getResolveSelectors,
|
|
205
|
+
getSuspendSelectors,
|
|
203
206
|
getActions,
|
|
204
207
|
subscribe,
|
|
205
208
|
};
|
|
@@ -376,6 +379,46 @@ function mapResolveSelectors( selectors, store ) {
|
|
|
376
379
|
} );
|
|
377
380
|
}
|
|
378
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Maps selectors to functions that throw a suspense promise if not yet resolved.
|
|
384
|
+
*
|
|
385
|
+
* @param {Object} selectors Selectors to map.
|
|
386
|
+
* @param {Object} store The redux store the selectors select from.
|
|
387
|
+
*
|
|
388
|
+
* @return {Object} Selectors mapped to their suspense functions.
|
|
389
|
+
*/
|
|
390
|
+
function mapSuspendSelectors( selectors, store ) {
|
|
391
|
+
return mapValues( selectors, ( selector, selectorName ) => {
|
|
392
|
+
// Selector without a resolver doesn't have any extra suspense behavior.
|
|
393
|
+
if ( ! selector.hasResolver ) {
|
|
394
|
+
return selector;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return ( ...args ) => {
|
|
398
|
+
const result = selector.apply( null, args );
|
|
399
|
+
|
|
400
|
+
if ( selectors.hasFinishedResolution( selectorName, args ) ) {
|
|
401
|
+
if ( selectors.hasResolutionFailed( selectorName, args ) ) {
|
|
402
|
+
throw selectors.getResolutionError( selectorName, args );
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
throw new Promise( ( resolve ) => {
|
|
409
|
+
const unsubscribe = store.subscribe( () => {
|
|
410
|
+
if (
|
|
411
|
+
selectors.hasFinishedResolution( selectorName, args )
|
|
412
|
+
) {
|
|
413
|
+
resolve();
|
|
414
|
+
unsubscribe();
|
|
415
|
+
}
|
|
416
|
+
} );
|
|
417
|
+
} );
|
|
418
|
+
};
|
|
419
|
+
} );
|
|
420
|
+
}
|
|
421
|
+
|
|
379
422
|
/**
|
|
380
423
|
* Returns resolvers with matched selectors for a given namespace.
|
|
381
424
|
* Resolvers are side effects invoked once per argument set of a given selector call,
|
package/src/registry.js
CHANGED
|
@@ -93,14 +93,16 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
|
|
|
93
93
|
return store.getSelectors();
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
return parent
|
|
96
|
+
return parent?.select( storeName );
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function __unstableMarkListeningStores( callback, ref ) {
|
|
100
100
|
listeningStores.clear();
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
try {
|
|
102
|
+
return callback.call( this );
|
|
103
|
+
} finally {
|
|
104
|
+
ref.current = Array.from( listeningStores );
|
|
105
|
+
}
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
/**
|
|
@@ -127,6 +129,29 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
|
|
|
127
129
|
return parent && parent.resolveSelect( storeName );
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Given the name of a registered store, returns an object containing the store's
|
|
134
|
+
* selectors pre-bound to state so that you only need to supply additional arguments,
|
|
135
|
+
* and modified so that they throw promises in case the selector is not resolved yet.
|
|
136
|
+
*
|
|
137
|
+
* @param {string|StoreDescriptor} storeNameOrDescriptor Unique namespace identifier for the store
|
|
138
|
+
* or the store descriptor.
|
|
139
|
+
*
|
|
140
|
+
* @return {Object} Object containing the store's suspense-wrapped selectors.
|
|
141
|
+
*/
|
|
142
|
+
function suspendSelect( storeNameOrDescriptor ) {
|
|
143
|
+
const storeName = isObject( storeNameOrDescriptor )
|
|
144
|
+
? storeNameOrDescriptor.name
|
|
145
|
+
: storeNameOrDescriptor;
|
|
146
|
+
listeningStores.add( storeName );
|
|
147
|
+
const store = stores[ storeName ];
|
|
148
|
+
if ( store ) {
|
|
149
|
+
return store.getSuspendSelectors();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return parent && parent.suspendSelect( storeName );
|
|
153
|
+
}
|
|
154
|
+
|
|
130
155
|
/**
|
|
131
156
|
* Returns the available actions for a part of the state.
|
|
132
157
|
*
|
|
@@ -276,6 +301,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
|
|
|
276
301
|
subscribe,
|
|
277
302
|
select,
|
|
278
303
|
resolveSelect,
|
|
304
|
+
suspendSelect,
|
|
279
305
|
dispatch,
|
|
280
306
|
use,
|
|
281
307
|
register,
|
package/tsconfig.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"compilerOptions": {
|
|
4
4
|
"rootDir": "src",
|
|
5
5
|
"declarationDir": "build-types",
|
|
6
|
-
"noUnusedParameters": false
|
|
6
|
+
"noUnusedParameters": false
|
|
7
7
|
},
|
|
8
8
|
"references": [
|
|
9
9
|
{ "path": "../compose" },
|
|
@@ -17,6 +17,6 @@
|
|
|
17
17
|
"src/redux-store/metadata/**/*",
|
|
18
18
|
"src/promise-middleware.js",
|
|
19
19
|
"src/utils",
|
|
20
|
-
|
|
20
|
+
"src/*.ts"
|
|
21
21
|
]
|
|
22
22
|
}
|