Skip to content

Commit 241f8ed

Browse files
author
Brian Vaughn
committed
DevTools: Replace legacy Suspense cache with unstable_getCacheForType
1 parent 2765955 commit 241f8ed

File tree

5 files changed

+113
-69
lines changed

5 files changed

+113
-69
lines changed

packages/react-devtools-shared/src/devtools/cache.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import type {Thenable} from 'shared/ReactTypes';
1212
import * as React from 'react';
1313
import {createContext} from 'react';
1414

15+
// TODO (cache) Remove this cache; it is outdated and will not work with newer APIs like startTransition.
16+
1517
// Cache implementation was forked from the React repo:
1618
// https://github.com/facebook/react/blob/master/packages/react-cache/src/ReactCache.js
1719
//

packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export default function InspectedElementWrapper(_: Props) {
3939
const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
4040

4141
const {
42+
clearErrorsForInspectedElement,
43+
clearWarningsForInspectedElement,
4244
copyInspectedElementPath,
4345
getInspectedElementPath,
4446
getInspectedElement,
@@ -228,6 +230,8 @@ export default function InspectedElementWrapper(_: Props) {
228230
key={
229231
inspectedElementID /* Force reset when selected Element changes */
230232
}
233+
clearErrorsForInspectedElement={clearErrorsForInspectedElement}
234+
clearWarningsForInspectedElement={clearWarningsForInspectedElement}
231235
copyInspectedElementPath={copyInspectedElementPath}
232236
element={element}
233237
getInspectedElementPath={getInspectedElementPath}

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js

Lines changed: 84 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import * as React from 'react';
1111
import {
1212
createContext,
13+
unstable_getCacheForType as getCacheForType,
14+
unstable_startTransition as startTransition,
15+
unstable_useCacheRefresh as useCacheRefresh,
1316
useCallback,
1417
useContext,
1518
useEffect,
@@ -18,7 +21,6 @@ import {
1821
useState,
1922
} from 'react';
2023
import {unstable_batchedUpdates as batchedUpdates} from 'react-dom';
21-
import {createResource} from '../../cache';
2224
import {BridgeContext, StoreContext} from '../context';
2325
import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
2426
import {TreeStateContext} from './TreeContext';
@@ -33,7 +35,6 @@ import type {
3335
Element,
3436
InspectedElement as InspectedElementFrontend,
3537
} from 'react-devtools-shared/src/devtools/views/Components/types';
36-
import type {Resource, Thenable} from '../../cache';
3738

3839
export type StoreAsGlobal = (id: number, path: Array<string | number>) => void;
3940

@@ -51,13 +52,15 @@ export type GetInspectedElement = (
5152
id: number,
5253
) => InspectedElementFrontend | null;
5354

54-
type RefreshInspectedElement = () => void;
55+
type ClearErrorsForInspectedElement = () => void;
56+
type ClearWarningsForInspectedElement = () => void;
5557

5658
export type InspectedElementContextType = {|
59+
clearErrorsForInspectedElement: ClearErrorsForInspectedElement,
60+
clearWarningsForInspectedElement: ClearWarningsForInspectedElement,
5761
copyInspectedElementPath: CopyInspectedElementPath,
5862
getInspectedElementPath: GetInspectedElementPath,
5963
getInspectedElement: GetInspectedElement,
60-
refreshInspectedElement: RefreshInspectedElement,
6164
storeAsGlobal: StoreAsGlobal,
6265
|};
6366

@@ -67,35 +70,21 @@ const InspectedElementContext = createContext<InspectedElementContextType>(
6770
InspectedElementContext.displayName = 'InspectedElementContext';
6871

6972
type ResolveFn = (inspectedElement: InspectedElementFrontend) => void;
70-
type InProgressRequest = {|
71-
promise: Thenable<InspectedElementFrontend>,
72-
resolveFn: ResolveFn,
73+
type Callback = (inspectedElement: InspectedElementFrontend) => void;
74+
type Thenable = {|
75+
callbacks: Set<Callback>,
76+
then: (callback: Callback) => void,
77+
resolve: ResolveFn,
7378
|};
7479

75-
const inProgressRequests: WeakMap<Element, InProgressRequest> = new WeakMap();
76-
const resource: Resource<
77-
Element,
80+
const inspectedElementThenables: WeakMap<Element, Thenable> = new WeakMap();
81+
82+
function createInspectedElementCache(): WeakMap<
7883
Element,
7984
InspectedElementFrontend,
80-
> = createResource(
81-
(element: Element) => {
82-
const request = inProgressRequests.get(element);
83-
if (request != null) {
84-
return request.promise;
85-
}
86-
87-
let resolveFn = ((null: any): ResolveFn);
88-
const promise = new Promise(resolve => {
89-
resolveFn = resolve;
90-
});
91-
92-
inProgressRequests.set(element, {promise, resolveFn});
93-
94-
return promise;
95-
},
96-
(element: Element) => element,
97-
{useWeakMap: true},
98-
);
85+
> {
86+
return new WeakMap();
87+
}
9988

10089
type Props = {|
10190
children: React$Node,
@@ -145,28 +134,75 @@ function InspectedElementContextController({children}: Props) {
145134
[bridge, store],
146135
);
147136

137+
// TODO (cache) Better encapsulate the cache and read/write methods.
138+
const inspectedElementCache = getCacheForType(createInspectedElementCache);
139+
148140
const getInspectedElement = useCallback<GetInspectedElement>(
149141
(id: number) => {
150142
const element = store.getElementByID(id);
151143
if (element !== null) {
152-
return resource.read(element);
153-
} else {
154-
return null;
144+
const maybeInspectedElement = inspectedElementCache.get(element);
145+
if (maybeInspectedElement !== undefined) {
146+
return maybeInspectedElement;
147+
} else {
148+
const maybeThenable = inspectedElementThenables.get(element);
149+
if (maybeThenable != null) {
150+
throw maybeThenable;
151+
}
152+
153+
const thenable: Thenable = {
154+
callbacks: new Set(),
155+
then: callback => {
156+
thenable.callbacks.add(callback);
157+
},
158+
resolve: inspectedElement => {
159+
thenable.callbacks.forEach(callback =>
160+
callback(inspectedElement),
161+
);
162+
},
163+
};
164+
165+
inspectedElementThenables.set(element, thenable);
166+
167+
throw thenable;
168+
}
155169
}
170+
return null;
156171
},
157-
[store],
172+
[inspectedElementCache, store],
158173
);
159174

160175
// It's very important that this context consumes selectedElementID and not inspectedElementID.
161176
// Otherwise the effect that sends the "inspect" message across the bridge-
162177
// would itself be blocked by the same render that suspends (waiting for the data).
163178
const {selectedElementID} = useContext(TreeStateContext);
164179

165-
const refreshInspectedElement = useCallback<RefreshInspectedElement>(() => {
180+
const refresh = useCacheRefresh();
181+
182+
const clearErrorsForInspectedElement = useCallback<ClearErrorsForInspectedElement>(() => {
166183
if (selectedElementID !== null) {
167184
const rendererID = store.getRendererIDForElement(selectedElementID);
168185
if (rendererID !== null) {
169186
bridge.send('inspectElement', {id: selectedElementID, rendererID});
187+
188+
startTransition(() => {
189+
store.clearErrorsForElement(selectedElementID);
190+
refresh();
191+
});
192+
}
193+
}
194+
}, [bridge, selectedElementID]);
195+
196+
const clearWarningsForInspectedElement = useCallback<ClearWarningsForInspectedElement>(() => {
197+
if (selectedElementID !== null) {
198+
const rendererID = store.getRendererIDForElement(selectedElementID);
199+
if (rendererID !== null) {
200+
bridge.send('inspectElement', {id: selectedElementID, rendererID});
201+
202+
startTransition(() => {
203+
store.clearWarningsForElement(selectedElementID);
204+
refresh();
205+
});
170206
}
171207
}
172208
}, [bridge, selectedElementID]);
@@ -198,7 +234,9 @@ function InspectedElementContextController({children}: Props) {
198234

199235
fillInPath(inspectedElement, data.value, data.path, value);
200236

201-
resource.write(element, inspectedElement);
237+
// TODO (cache) This mutation seems sketchy.
238+
// Probably need to refresh the cache with a new seed.
239+
inspectedElementCache.set(element, inspectedElement);
202240

203241
// Schedule update with React if the currently-selected element has been invalidated.
204242
if (id === selectedElementID) {
@@ -277,16 +315,17 @@ function InspectedElementContextController({children}: Props) {
277315

278316
element = store.getElementByID(id);
279317
if (element !== null) {
280-
const request = inProgressRequests.get(element);
281-
if (request != null) {
282-
inProgressRequests.delete(element);
318+
inspectedElementCache.set(element, inspectedElement);
319+
320+
const thenable = inspectedElementThenables.get(element);
321+
if (thenable != null) {
322+
inspectedElementThenables.delete(element);
323+
283324
batchedUpdates(() => {
284-
request.resolveFn(inspectedElement);
325+
thenable.resolve(inspectedElement);
285326
setCurrentlyInspectedElement(inspectedElement);
286327
});
287328
} else {
288-
resource.write(element, inspectedElement);
289-
290329
// Schedule update with React if the currently-selected element has been invalidated.
291330
if (id === selectedElementID) {
292331
setCurrentlyInspectedElement(inspectedElement);
@@ -356,19 +395,21 @@ function InspectedElementContextController({children}: Props) {
356395

357396
const value = useMemo(
358397
() => ({
398+
clearErrorsForInspectedElement,
399+
clearWarningsForInspectedElement,
359400
copyInspectedElementPath,
360401
getInspectedElement,
361402
getInspectedElementPath,
362-
refreshInspectedElement,
363403
storeAsGlobal,
364404
}),
365405
// InspectedElement is used to invalidate the cache and schedule an update with React.
366406
[
407+
clearErrorsForInspectedElement,
408+
clearWarningsForInspectedElement,
367409
copyInspectedElementPath,
368410
currentlyInspectedElement,
369411
getInspectedElement,
370412
getInspectedElementPath,
371-
refreshInspectedElement,
372413
storeAsGlobal,
373414
],
374415
);

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,7 @@
5858
background-color: var(--color-console-warning-icon);
5959
color: var(--color-console-warning-badge-text);
6060
}
61+
62+
.Pending {
63+
filter: grayscale(0.75);
64+
}

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorsAndWarningsTree.js

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import * as React from 'react';
11-
import {useContext} from 'react';
11+
import {useContext, unstable_useTransition as useTransition} from 'react';
1212
import Button from '../Button';
1313
import ButtonIcon from '../ButtonIcon';
1414
import Store from '../../store';
@@ -31,7 +31,10 @@ export default function InspectedElementErrorsAndWarningsTree({
3131
inspectedElement,
3232
store,
3333
}: Props) {
34-
const {refreshInspectedElement} = useContext(InspectedElementContext);
34+
const {
35+
clearErrorsForInspectedElement,
36+
clearWarningsForInspectedElement,
37+
} = useContext(InspectedElementContext);
3538

3639
const {showInlineWarningsAndErrors} = useContext(SettingsContext);
3740
if (!showInlineWarningsAndErrors) {
@@ -40,34 +43,14 @@ export default function InspectedElementErrorsAndWarningsTree({
4043

4144
const {errors, warnings} = inspectedElement;
4245

43-
const clearErrors = () => {
44-
const {id} = inspectedElement;
45-
store.clearErrorsForElement(id);
46-
47-
// Immediately poll for updated data.
48-
// This avoids a delay between clicking the clear button and refreshing errors.
49-
// Ideally this would be done with useTranstion but that requires updating to a newer Cache strategy.
50-
refreshInspectedElement();
51-
};
52-
53-
const clearWarnings = () => {
54-
const {id} = inspectedElement;
55-
store.clearWarningsForElement(id);
56-
57-
// Immediately poll for updated data.
58-
// This avoids a delay between clicking the clear button and refreshing warnings.
59-
// Ideally this would be done with useTranstion but that requires updating to a newer Cache strategy.
60-
refreshInspectedElement();
61-
};
62-
6346
return (
6447
<React.Fragment>
6548
{errors.length > 0 && (
6649
<Tree
6750
badgeClassName={styles.ErrorBadge}
6851
bridge={bridge}
6952
className={styles.ErrorTree}
70-
clearMessages={clearErrors}
53+
clearMessages={clearErrorsForInspectedElement}
7154
entries={errors}
7255
label="errors"
7356
messageClassName={styles.Error}
@@ -78,7 +61,7 @@ export default function InspectedElementErrorsAndWarningsTree({
7861
badgeClassName={styles.WarningBadge}
7962
bridge={bridge}
8063
className={styles.WarningTree}
81-
clearMessages={clearWarnings}
64+
clearMessages={clearWarningsForInspectedElement}
8265
entries={warnings}
8366
label="warnings"
8467
messageClassName={styles.Warning}
@@ -107,6 +90,8 @@ function Tree({
10790
label,
10891
messageClassName,
10992
}: TreeProps) {
93+
const [startTransition, isPending] = useTransition();
94+
11095
if (entries.length === 0) {
11196
return null;
11297
}
@@ -115,7 +100,12 @@ function Tree({
115100
<div className={`${sharedStyles.HeaderRow} ${styles.HeaderRow}`}>
116101
<div className={sharedStyles.Header}>{label}</div>
117102
<Button
118-
onClick={clearMessages}
103+
disabled={isPending}
104+
onClick={() =>
105+
startTransition(() => {
106+
clearMessages();
107+
})
108+
}
119109
title={`Clear all ${label} for this component`}>
120110
<ButtonIcon type="clear" />
121111
</Button>
@@ -126,6 +116,7 @@ function Tree({
126116
badgeClassName={badgeClassName}
127117
className={messageClassName}
128118
count={count}
119+
isPending={isPending}
129120
message={message}
130121
/>
131122
))}
@@ -137,17 +128,19 @@ type ErrorOrWarningViewProps = {|
137128
badgeClassName: string,
138129
className: string,
139130
count: number,
131+
isPending: boolean,
140132
message: string,
141133
|};
142134

143135
function ErrorOrWarningView({
144136
className,
145137
badgeClassName,
146138
count,
139+
isPending,
147140
message,
148141
}: ErrorOrWarningViewProps) {
149142
return (
150-
<div className={className}>
143+
<div className={`${className} ${isPending ? styles.Pending : ''}`}>
151144
{count > 1 && <div className={badgeClassName}>{count}</div>}
152145
<div className={styles.Message} title={message}>
153146
{message}

0 commit comments

Comments
 (0)