-
Notifications
You must be signed in to change notification settings - Fork 4.2k
/
index.js
184 lines (167 loc) · 5.56 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/**
* External dependencies
*/
import { useMemoOne } from 'use-memo-one';
/**
* WordPress dependencies
*/
import { createQueue } from '@wordpress/priority-queue';
import {
useLayoutEffect,
useRef,
useCallback,
useEffect,
useReducer,
} from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';
/**
* Internal dependencies
*/
import useRegistry from '../registry-provider/use-registry';
import useAsyncMode from '../async-mode-provider/use-async-mode';
/**
* Favor useLayoutEffect to ensure the store subscription callback always has
* the selector from the latest render. If a store update happens between render
* and the effect, this could cause missed/stale updates or inconsistent state.
*
* Fallback to useEffect for server rendered components because currently React
* throws a warning when using useLayoutEffect in that environment.
*/
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
const renderQueue = createQueue();
/**
* Custom react hook for retrieving props from registered selectors.
*
* In general, this custom React hook follows the
* [rules of hooks](https://reactjs.org/docs/hooks-rules.html).
*
* @param {Function} _mapSelect Function called on every state change. The
* returned value is exposed to the component
* implementing this hook. The function receives
* the `registry.select` method on the first
* argument and the `registry` on the second
* argument.
* @param {Array} deps If provided, this memoizes the mapSelect so the
* same `mapSelect` is invoked on every state
* change unless the dependencies change.
*
* @example
* ```js
* const { useSelect } = wp.data;
*
* function HammerPriceDisplay( { currency } ) {
* const price = useSelect( ( select ) => {
* return select( 'my-shop' ).getPrice( 'hammer', currency )
* }, [ currency ] );
* return new Intl.NumberFormat( 'en-US', {
* style: 'currency',
* currency,
* } ).format( price );
* }
*
* // Rendered in the application:
* // <HammerPriceDisplay currency="USD" />
* ```
*
* In the above example, when `HammerPriceDisplay` is rendered into an
* application, the price will be retrieved from the store state using the
* `mapSelect` callback on `useSelect`. If the currency prop changes then
* any price in the state for that currency is retrieved. If the currency prop
* doesn't change and other props are passed in that do change, the price will
* not change because the dependency is just the currency.
*
* @return {Function} A custom react hook.
*/
export default function useSelect( _mapSelect, deps ) {
const mapSelect = useCallback( _mapSelect, deps );
const registry = useRegistry();
const isAsync = useAsyncMode();
// React can sometimes clear the `useMemo` cache.
// We use the cache-stable `useMemoOne` to avoid
// losing queues.
const queueContext = useMemoOne( () => ( { queue: true } ), [ registry ] );
const [ , forceRender ] = useReducer( ( s ) => s + 1, 0 );
const latestMapSelect = useRef();
const latestIsAsync = useRef( isAsync );
const latestMapOutput = useRef();
const latestMapOutputError = useRef();
const isMountedAndNotUnsubscribing = useRef();
let mapOutput;
try {
if (
latestMapSelect.current !== mapSelect ||
latestMapOutputError.current
) {
mapOutput = mapSelect( registry.select, registry );
} else {
mapOutput = latestMapOutput.current;
}
} catch ( error ) {
let errorMessage = `An error occurred while running 'mapSelect': ${ error.message }`;
if ( latestMapOutputError.current ) {
errorMessage += `\nThe error may be correlated with this previous error:\n`;
errorMessage += `${ latestMapOutputError.current.stack }\n\n`;
errorMessage += 'Original stack trace:';
throw new Error( errorMessage );
} else {
// eslint-disable-next-line no-console
console.error( errorMessage );
}
}
useIsomorphicLayoutEffect( () => {
latestMapSelect.current = mapSelect;
latestMapOutput.current = mapOutput;
latestMapOutputError.current = undefined;
isMountedAndNotUnsubscribing.current = true;
// This has to run after the other ref updates
// to avoid using stale values in the flushed
// callbacks or potentially overwriting a
// changed `latestMapOutput.current`.
if ( latestIsAsync.current !== isAsync ) {
latestIsAsync.current = isAsync;
renderQueue.flush( queueContext );
}
} );
useIsomorphicLayoutEffect( () => {
const onStoreChange = () => {
if ( isMountedAndNotUnsubscribing.current ) {
try {
const newMapOutput = latestMapSelect.current(
registry.select,
registry
);
if (
isShallowEqual( latestMapOutput.current, newMapOutput )
) {
return;
}
latestMapOutput.current = newMapOutput;
} catch ( error ) {
latestMapOutputError.current = error;
}
forceRender();
}
};
// catch any possible state changes during mount before the subscription
// could be set.
if ( latestIsAsync.current ) {
renderQueue.add( queueContext, onStoreChange );
} else {
onStoreChange();
}
const unsubscribe = registry.subscribe( () => {
if ( latestIsAsync.current ) {
renderQueue.add( queueContext, onStoreChange );
} else {
onStoreChange();
}
} );
return () => {
isMountedAndNotUnsubscribing.current = false;
unsubscribe();
renderQueue.flush( queueContext );
};
}, [ registry ] );
return mapOutput;
}