@wordpress/data 6.7.0 → 6.10.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 +6 -0
- package/README.md +28 -0
- package/build/components/use-select/index.js +133 -19
- 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/plugins/persistence/index.js +2 -375
- package/build/plugins/persistence/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 +131 -19
- 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/plugins/persistence/index.js +2 -365
- package/build-module/plugins/persistence/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 +144 -14
- package/src/components/use-select/test/index.js +139 -3
- package/src/components/use-select/test/suspense.js +160 -0
- package/src/index.js +16 -1
- package/src/plugins/persistence/index.js +2 -420
- package/src/plugins/persistence/test/index.js +1 -777
- package/src/redux-store/index.js +43 -0
- package/src/registry.js +30 -4
- package/src/test/registry.js +8 -1
- package/tsconfig.json +2 -2
|
@@ -118,6 +118,71 @@ describe( 'useSelect', () => {
|
|
|
118
118
|
expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 'bar' );
|
|
119
119
|
} );
|
|
120
120
|
|
|
121
|
+
it( 'avoid calling nested listener after unmounted', async () => {
|
|
122
|
+
registry.registerStore( 'toggler', {
|
|
123
|
+
reducer: ( state = false, action ) =>
|
|
124
|
+
action.type === 'TOGGLE' ? ! state : state,
|
|
125
|
+
actions: {
|
|
126
|
+
toggle: () => ( { type: 'TOGGLE' } ),
|
|
127
|
+
},
|
|
128
|
+
selectors: {
|
|
129
|
+
get: ( state ) => state,
|
|
130
|
+
},
|
|
131
|
+
} );
|
|
132
|
+
|
|
133
|
+
const mapSelect = ( select ) => select( 'toggler' ).get();
|
|
134
|
+
|
|
135
|
+
const mapSelectChild = jest.fn( mapSelect );
|
|
136
|
+
function Child() {
|
|
137
|
+
const show = useSelect( mapSelectChild, [] );
|
|
138
|
+
return show ? 'yes' : 'no';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const mapSelectParent = jest.fn( mapSelect );
|
|
142
|
+
function Parent() {
|
|
143
|
+
const show = useSelect( mapSelectParent, [] );
|
|
144
|
+
return show ? <Child /> : 'none';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const rendered = render(
|
|
148
|
+
<RegistryProvider value={ registry }>
|
|
149
|
+
<Parent />
|
|
150
|
+
</RegistryProvider>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Initial render renders only parent and subscribes the parent to store.
|
|
154
|
+
expect( rendered.getByText( 'none' ) ).toBeInTheDocument();
|
|
155
|
+
expect( mapSelectParent ).toHaveBeenCalledTimes( 2 );
|
|
156
|
+
expect( mapSelectChild ).toHaveBeenCalledTimes( 0 );
|
|
157
|
+
|
|
158
|
+
// act() does batched updates internally, i.e., any scheduled setStates or effects
|
|
159
|
+
// will be executed only after the dispatch finishes. But we want to opt out of
|
|
160
|
+
// batched updates here. We want all the setStates to be done synchronously, as the
|
|
161
|
+
// store listeners are called. The async/await code is a trick to do it: do the
|
|
162
|
+
// dispatch in a different event loop tick, where the batched updates are no longer active.
|
|
163
|
+
await act( async () => {
|
|
164
|
+
await Promise.resolve();
|
|
165
|
+
registry.dispatch( 'toggler' ).toggle();
|
|
166
|
+
} );
|
|
167
|
+
|
|
168
|
+
// Child was rendered and subscribed to the store, as the _second_ subscription.
|
|
169
|
+
expect( rendered.getByText( 'yes' ) ).toBeInTheDocument();
|
|
170
|
+
expect( mapSelectParent ).toHaveBeenCalledTimes( 3 );
|
|
171
|
+
expect( mapSelectChild ).toHaveBeenCalledTimes( 2 );
|
|
172
|
+
|
|
173
|
+
await act( async () => {
|
|
174
|
+
await Promise.resolve();
|
|
175
|
+
registry.dispatch( 'toggler' ).toggle();
|
|
176
|
+
} );
|
|
177
|
+
|
|
178
|
+
// Check that child was unmounted without any extra state update being performed on it.
|
|
179
|
+
// I.e., `mapSelectChild` was never called again, and no "state update on an unmounted
|
|
180
|
+
// component" warning was triggered.
|
|
181
|
+
expect( rendered.getByText( 'none' ) ).toBeInTheDocument();
|
|
182
|
+
expect( mapSelectParent ).toHaveBeenCalledTimes( 4 );
|
|
183
|
+
expect( mapSelectChild ).toHaveBeenCalledTimes( 2 );
|
|
184
|
+
} );
|
|
185
|
+
|
|
121
186
|
describe( 'rerenders as expected with various mapSelect return types', () => {
|
|
122
187
|
const getComponent = ( mapSelectSpy ) => () => {
|
|
123
188
|
const data = useSelect( mapSelectSpy, [] );
|
|
@@ -819,10 +884,10 @@ describe( 'useSelect', () => {
|
|
|
819
884
|
// Ensure the async update was flushed during the rerender.
|
|
820
885
|
expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 1 );
|
|
821
886
|
|
|
822
|
-
// initial render + subscription check +
|
|
887
|
+
// initial render + subscription check + rerender with isAsync=false
|
|
823
888
|
expect( selectSpy ).toHaveBeenCalledTimes( 3 );
|
|
824
|
-
// initial render + rerender with isAsync=false
|
|
825
|
-
expect( TestComponent ).toHaveBeenCalledTimes(
|
|
889
|
+
// initial render + rerender with isAsync=false
|
|
890
|
+
expect( TestComponent ).toHaveBeenCalledTimes( 2 );
|
|
826
891
|
} );
|
|
827
892
|
|
|
828
893
|
it( 'cancels scheduled updates when mapSelect function changes', async () => {
|
|
@@ -960,4 +1025,75 @@ describe( 'useSelect', () => {
|
|
|
960
1025
|
expect( TestComponent ).toHaveBeenCalledTimes( 2 );
|
|
961
1026
|
} );
|
|
962
1027
|
} );
|
|
1028
|
+
|
|
1029
|
+
describe( 'usage without dependencies array', () => {
|
|
1030
|
+
function registerStore( name, initial ) {
|
|
1031
|
+
registry.registerStore( name, {
|
|
1032
|
+
reducer: ( s = initial, a ) => ( a.type === 'inc' ? s + 1 : s ),
|
|
1033
|
+
actions: { inc: () => ( { type: 'inc' } ) },
|
|
1034
|
+
selectors: { get: ( s ) => s },
|
|
1035
|
+
} );
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
it( 'does not memoize the callback when there are no deps', () => {
|
|
1039
|
+
registerStore( 'store', 1 );
|
|
1040
|
+
|
|
1041
|
+
const Status = ( { multiple } ) => {
|
|
1042
|
+
const count = useSelect(
|
|
1043
|
+
( select ) => select( 'store' ).get() * multiple
|
|
1044
|
+
);
|
|
1045
|
+
return <div role="status">{ count }</div>;
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
const App = ( { multiple } ) => (
|
|
1049
|
+
<RegistryProvider value={ registry }>
|
|
1050
|
+
<Status multiple={ multiple } />
|
|
1051
|
+
</RegistryProvider>
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
const rendered = render( <App multiple={ 1 } /> );
|
|
1055
|
+
expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 1 );
|
|
1056
|
+
|
|
1057
|
+
// Check that the most recent value of `multiple` is used to render:
|
|
1058
|
+
// the old callback wasn't memoized and there is no stale closure problem.
|
|
1059
|
+
rendered.rerender( <App multiple={ 2 } /> );
|
|
1060
|
+
expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 2 );
|
|
1061
|
+
} );
|
|
1062
|
+
|
|
1063
|
+
it( 'subscribes only stores used by the initial callback', () => {
|
|
1064
|
+
registerStore( 'counter-1', 1 );
|
|
1065
|
+
registerStore( 'counter-2', 10 );
|
|
1066
|
+
|
|
1067
|
+
const Status = ( { store } ) => {
|
|
1068
|
+
const count = useSelect( ( select ) => select( store ).get() );
|
|
1069
|
+
return <div role="status">{ count }</div>;
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
const App = ( { store } ) => (
|
|
1073
|
+
<RegistryProvider value={ registry }>
|
|
1074
|
+
<Status store={ store } />
|
|
1075
|
+
</RegistryProvider>
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
// initial render with counter-1
|
|
1079
|
+
const rendered = render( <App store="counter-1" /> );
|
|
1080
|
+
expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 1 );
|
|
1081
|
+
|
|
1082
|
+
// update from counter-1
|
|
1083
|
+
act( () => {
|
|
1084
|
+
registry.dispatch( 'counter-1' ).inc();
|
|
1085
|
+
} );
|
|
1086
|
+
expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 2 );
|
|
1087
|
+
|
|
1088
|
+
// rerender with counter-2
|
|
1089
|
+
rendered.rerender( <App store="counter-2" /> );
|
|
1090
|
+
expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 10 );
|
|
1091
|
+
|
|
1092
|
+
// update from counter-2 is ignored because component is subcribed only to counter-1
|
|
1093
|
+
act( () => {
|
|
1094
|
+
registry.dispatch( 'counter-2' ).inc();
|
|
1095
|
+
} );
|
|
1096
|
+
expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 10 );
|
|
1097
|
+
} );
|
|
1098
|
+
} );
|
|
963
1099
|
} );
|
|
@@ -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.
|