Skip to content

Commit 87a82a5

Browse files
authored
Merge pull request #45 from atheck/beta
feat: add support for Redux Dev Tools Extension
2 parents d6bf6bf + bb34a93 commit 87a82a5

File tree

4 files changed

+91
-1
lines changed

4 files changed

+91
-1
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ This library brings the elmish pattern to react.
3232
- [Combine update and execCmd](#combine-update-and-execcmd)
3333
- [Testing subscriptions](#testing-subscriptions)
3434
- [UI Tests](#ui-tests)
35+
- [Redux Dev Tools](#redux-dev-tools)
3536
- [Migrations](#migrations)
3637
- [From v1.x to v2.x](#from-v1x-to-v2x)
3738
- [From v2.x to v3.x](#from-v2x-to-v3x)
@@ -1077,6 +1078,20 @@ it("dispatches the correct message", async () => {
10771078
10781079
This works for function components using the `useElmish` hook and class components.
10791080
1081+
## Redux Dev Tools
1082+
1083+
If you have the Redux Dev Tools installed in your browser, you can enable support for this extension by setting the `enableDevTools` option to `true` in the `init` function.
1084+
1085+
```ts
1086+
import { init } from "react-elmish";
1087+
1088+
init({
1089+
enableDevTools: true,
1090+
});
1091+
```
1092+
1093+
Hint: You should only enable this in development mode.
1094+
10801095
## Migrations
10811096
10821097
### From v1.x to v2.x

src/Init.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ interface ElmOptions {
2727
* @type {DispatchMiddlewareFunc}
2828
*/
2929
dispatchMiddleware?: DispatchMiddlewareFunc;
30+
31+
enableDevTools?: boolean;
3032
}
3133

3234
const Services: ElmOptions = {
3335
logger: undefined,
3436
errorMiddleware: undefined,
3537
dispatchMiddleware: undefined,
38+
enableDevTools: false,
3639
};
3740

3841
/**
@@ -44,6 +47,7 @@ function init(options: ElmOptions): void {
4447
Services.logger = options.logger;
4548
Services.errorMiddleware = options.errorMiddleware;
4649
Services.dispatchMiddleware = options.dispatchMiddleware;
50+
Services.enableDevTools = options.enableDevTools;
4751
}
4852

4953
export type { DispatchMiddlewareFunc, ElmOptions, ErrorMiddlewareFunc, Logger };

src/reduxDevTools.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
interface ReduxDevToolsExtension {
2+
connect: (options?: ReduxOptions) => ReduxDevTools;
3+
disconnect: () => void;
4+
}
5+
6+
interface ReduxOptions {
7+
name?: string;
8+
serialize?: {
9+
options?: boolean;
10+
};
11+
}
12+
13+
interface ReduxDevTools {
14+
init: (state: unknown) => void;
15+
send: (message: string, state: unknown, options?: ReduxOptions) => void;
16+
subscribe: (callback: (message: ReduxMessage) => void) => () => void;
17+
unsubscribe: () => void;
18+
}
19+
20+
interface ReduxMessage {
21+
type: string;
22+
payload: {
23+
type: string;
24+
};
25+
state: string;
26+
}
27+
28+
interface ReduxDevToolsExtensionWindow extends Window {
29+
// biome-ignore lint/style/useNamingConvention: Predefined
30+
__REDUX_DEVTOOLS_EXTENSION__: ReduxDevToolsExtension;
31+
}
32+
33+
declare global {
34+
interface Window {
35+
// biome-ignore lint/style/useNamingConvention: Predefined
36+
__REDUX_DEVTOOLS_EXTENSION__?: ReduxDevToolsExtension;
37+
}
38+
}
39+
40+
function isReduxDevToolsEnabled(window: Window | undefined): window is ReduxDevToolsExtensionWindow {
41+
return window !== undefined && "__REDUX_DEVTOOLS_EXTENSION__" in window;
42+
}
43+
44+
export type { ReduxDevTools, ReduxDevToolsExtension };
45+
46+
export { isReduxDevToolsEnabled };

src/useElmish.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
import { createCallBase } from "./createCallBase";
1717
import { createDefer } from "./createDefer";
1818
import { getFakeOptionsOnce } from "./fakeOptions";
19+
import { isReduxDevToolsEnabled, type ReduxDevTools } from "./reduxDevTools";
1920

2021
/**
2122
* The return type of the `subscription` function.
@@ -81,13 +82,31 @@ function useElmish<TProps, TModel, TMessage extends Message>({
8182
const propsRef = useRef(props);
8283
const isMountedRef = useRef(true);
8384

85+
const devTools = useRef<ReduxDevTools | null>(null);
86+
8487
useEffect(() => {
88+
let reduxUnsubscribe: (() => void) | undefined;
89+
90+
if (Services.enableDevTools === true && isReduxDevToolsEnabled(window)) {
91+
// eslint-disable-next-line no-underscore-dangle
92+
devTools.current = window.__REDUX_DEVTOOLS_EXTENSION__.connect({ name, serialize: { options: true } });
93+
94+
reduxUnsubscribe = devTools.current.subscribe((message) => {
95+
if (message.type === "DISPATCH" && message.payload.type === "JUMP_TO_ACTION") {
96+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
97+
setModel(JSON.parse(message.state) as TModel);
98+
}
99+
});
100+
}
101+
85102
isMountedRef.current = true;
86103

87104
return () => {
88105
isMountedRef.current = false;
106+
107+
reduxUnsubscribe?.();
89108
};
90-
}, []);
109+
}, [name]);
91110

92111
let initializedModel = model;
93112

@@ -115,6 +134,10 @@ function useElmish<TProps, TModel, TMessage extends Message>({
115134
modified = true;
116135
}
117136

137+
if (devTools.current) {
138+
devTools.current.send(nextMsg.name, { ...initializedModel, ...currentModel });
139+
}
140+
118141
nextMsg = buffer.shift();
119142
} while (nextMsg);
120143

@@ -140,6 +163,8 @@ function useElmish<TProps, TModel, TMessage extends Message>({
140163
initializedModel = initModel;
141164
setModel(initializedModel);
142165

166+
devTools.current?.init(initializedModel);
167+
143168
Services.logger?.debug("Initial model for", name, initializedModel);
144169

145170
execCmd(dispatch, ...initCommands);

0 commit comments

Comments
 (0)