Skip to content

Commit 2fca2e8

Browse files
authored
refactor(utils): extract redux extension logic into wrapper functions (#38)
* refactor(utils): extract getReduxExtension * refactor(utils): extract createReduxConnection
1 parent 59b1564 commit 2fca2e8

File tree

4 files changed

+109
-66
lines changed

4 files changed

+109
-66
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Message } from '../types';
2+
import { ReduxExtension } from './getReduxExtension';
3+
4+
// Original but incomplete type of the redux extension package
5+
type ConnectResponse = ReturnType<NonNullable<ReduxExtension>['connect']>;
6+
7+
export type Connection = {
8+
/** Mark the connection as not initiated, so it can be initiated before using it. */
9+
shouldInit?: boolean;
10+
11+
/** Initiate the connection and add it to the extension connections.
12+
* Should only be executed once in the live time of the connection.
13+
*/
14+
init: ConnectResponse['init'];
15+
16+
// FIXME https://github.com/reduxjs/redux-devtools/issues/1097
17+
/** Add a subscription to the connection.
18+
* The provided listener will be executed when the user interacts with the extension
19+
* with actions like time traveling, importing a state or the likes.
20+
*
21+
* @param listener function to be executed when an action is submitted
22+
* @returns function to unsubscribe the applied listener
23+
*/
24+
subscribe: (listener: (message: Message) => void) => (() => void) | undefined;
25+
26+
/** Send a new action to the connection to display the state change in the extension.
27+
* For example when the value of the store changes.
28+
*/
29+
send: ConnectResponse['send'];
30+
};
31+
32+
/** Wrapper for creating connections to the redux extension
33+
* Connections are used to display the stores value and value changes within the extension
34+
* as well as reacting to extension actions like time traveling.
35+
**/
36+
export const createReduxConnection = (
37+
extension: ReduxExtension | undefined,
38+
name: string,
39+
) => {
40+
if (!extension) return undefined;
41+
const connection = extension.connect({ name });
42+
43+
return Object.assign(connection, {
44+
shouldInit: true,
45+
}) as Connection;
46+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Original but incomplete type of the redux extension package
2+
type Extension = NonNullable<typeof window.__REDUX_DEVTOOLS_EXTENSION__>;
3+
4+
export type ReduxExtension = {
5+
/** Create a connection to the extension.
6+
* This will connect a store (like an atom) to the extension and
7+
* display it within the extension tab.
8+
*
9+
* @param options https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md
10+
* @returns https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Methods.md#connectoptions
11+
*/
12+
connect: Extension['connect'];
13+
14+
/** Disconnects all existing connections to the redux extension.
15+
* Only use this when you are sure that no other connection exists
16+
* or you want to remove all existing connections.
17+
*/
18+
disconnect?: () => void;
19+
20+
/** Have a look at the documentation for more methods:
21+
* https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Methods.md
22+
*/
23+
};
24+
25+
/** Returns the global redux extension object if available */
26+
export const getReduxExtension = (
27+
enabled = __DEV__,
28+
): ReduxExtension | undefined => {
29+
if (!enabled) {
30+
return undefined;
31+
}
32+
33+
const reduxExtension = window.__REDUX_DEVTOOLS_EXTENSION__;
34+
if (!reduxExtension && __DEV__) {
35+
console.warn('Please install/enable Redux devtools extension');
36+
return undefined;
37+
}
38+
39+
return reduxExtension;
40+
};

src/utils/useAtomDevtools.ts

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { useEffect, useRef } from 'react';
22
import { useAtom } from 'jotai/react';
33
import type { Atom, WritableAtom } from 'jotai/vanilla';
4-
import { Message } from './types';
4+
import {
5+
Connection,
6+
createReduxConnection,
7+
} from './redux-extension/createReduxConnection';
8+
import { getReduxExtension } from './redux-extension/getReduxExtension';
59

610
type DevtoolOptions = Parameters<typeof useAtom>[1] & {
711
name?: string;
@@ -14,31 +18,13 @@ export function useAtomDevtools<Value, Result>(
1418
): void {
1519
const { enabled, name } = options || {};
1620

17-
let extension: typeof window['__REDUX_DEVTOOLS_EXTENSION__'] | false;
18-
19-
try {
20-
extension = (enabled ?? __DEV__) && window.__REDUX_DEVTOOLS_EXTENSION__;
21-
} catch {
22-
// ignored
23-
}
24-
25-
if (!extension) {
26-
if (__DEV__ && enabled) {
27-
console.warn('Please install/enable Redux devtools extension');
28-
}
29-
}
21+
const extension = getReduxExtension(enabled);
3022

3123
const [value, setValue] = useAtom(anAtom, options);
3224

3325
const lastValue = useRef(value);
3426
const isTimeTraveling = useRef(false);
35-
const devtools = useRef<
36-
ReturnType<
37-
NonNullable<typeof window['__REDUX_DEVTOOLS_EXTENSION__']>['connect']
38-
> & {
39-
shouldInit?: boolean;
40-
}
41-
>();
27+
const devtools = useRef<Connection>();
4228

4329
const atomName = name || anAtom.debugLabel || anAtom.toString();
4430

@@ -57,16 +43,9 @@ export function useAtomDevtools<Value, Result>(
5743
);
5844
};
5945

60-
devtools.current = extension.connect({ name: atomName });
46+
devtools.current = createReduxConnection(extension, atomName);
6147

62-
const unsubscribe = (
63-
devtools.current as unknown as {
64-
// FIXME https://github.com/reduxjs/redux-devtools/issues/1097
65-
subscribe: (
66-
listener: (message: Message) => void,
67-
) => (() => void) | undefined;
68-
}
69-
).subscribe((message) => {
48+
const unsubscribe = devtools.current?.subscribe((message) => {
7049
if (message.type === 'ACTION' && message.payload) {
7150
try {
7251
setValueIfWritable(JSON.parse(message.payload));
@@ -106,7 +85,7 @@ export function useAtomDevtools<Value, Result>(
10685
});
10786
}
10887
});
109-
devtools.current.shouldInit = true;
88+
11089
return unsubscribe;
11190
}, [anAtom, extension, atomName, setValue]);
11291

src/utils/useAtomsDevtools.ts

Lines changed: 13 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { useEffect, useRef } from 'react';
22
import { AnyAtom, AnyAtomValue, AtomsSnapshot, Options } from '../types';
3-
import { Message } from './types';
3+
import {
4+
Connection,
5+
createReduxConnection,
6+
} from './redux-extension/createReduxConnection';
7+
import { getReduxExtension } from './redux-extension/getReduxExtension';
48
import { useAtomsSnapshot } from './useAtomsSnapshot';
59
import { useGotoAtomsSnapshot } from './useGotoAtomsSnapshot';
610

@@ -32,33 +36,15 @@ export function useAtomsDevtools(
3236
): void {
3337
const { enabled } = options || {};
3438

35-
let extension: typeof window['__REDUX_DEVTOOLS_EXTENSION__'] | false;
36-
37-
try {
38-
extension = (enabled ?? __DEV__) && window.__REDUX_DEVTOOLS_EXTENSION__;
39-
} catch {
40-
// ignored
41-
}
42-
43-
if (!extension) {
44-
if (__DEV__ && enabled) {
45-
console.warn('Please install/enable Redux devtools extension');
46-
}
47-
}
39+
const extension = getReduxExtension(enabled);
4840

4941
// This an exception, we don't usually use utils in themselves!
5042
const atomsSnapshot = useAtomsSnapshot(options);
5143
const goToSnapshot = useGotoAtomsSnapshot(options);
5244

5345
const isTimeTraveling = useRef(false);
5446
const isRecording = useRef(true);
55-
const devtools = useRef<
56-
ReturnType<
57-
NonNullable<typeof window['__REDUX_DEVTOOLS_EXTENSION__']>['connect']
58-
> & {
59-
shouldInit?: boolean;
60-
}
61-
>();
47+
const devtools = useRef<Connection>();
6248

6349
const snapshots = useRef<AtomsSnapshot[]>([]);
6450

@@ -74,16 +60,10 @@ export function useAtomsDevtools(
7460
}
7561
return snapshot;
7662
};
77-
const connection = extension.connect({ name });
78-
79-
const devtoolsUnsubscribe = (
80-
connection as unknown as {
81-
// FIXME https://github.com/reduxjs/redux-devtools/issues/1097
82-
subscribe: (
83-
listener: (message: Message) => void,
84-
) => (() => void) | undefined;
85-
}
86-
).subscribe((message) => {
63+
64+
devtools.current = createReduxConnection(extension, name);
65+
66+
const devtoolsUnsubscribe = devtools.current?.subscribe((message) => {
8767
switch (message.type) {
8868
case 'DISPATCH':
8969
switch (message.payload?.type) {
@@ -92,7 +72,7 @@ export function useAtomsDevtools(
9272
break;
9373

9474
case 'COMMIT':
95-
connection.init(getDevtoolsState(getSnapshotAt()));
75+
devtools.current?.init(getDevtoolsState(getSnapshotAt()));
9676
snapshots.current = [];
9777
break;
9878

@@ -109,10 +89,8 @@ export function useAtomsDevtools(
10989
}
11090
});
11191

112-
devtools.current = connection;
113-
devtools.current.shouldInit = true;
11492
return () => {
115-
(extension as any).disconnect();
93+
extension?.disconnect?.();
11694
devtoolsUnsubscribe?.();
11795
};
11896
}, [extension, goToSnapshot, name]);

0 commit comments

Comments
 (0)