Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ This library brings the elmish pattern to react.
- [Combine update and execCmd](#combine-update-and-execcmd)
- [Testing subscriptions](#testing-subscriptions)
- [UI Tests](#ui-tests)
- [Redux Dev Tools](#redux-dev-tools)
- [Migrations](#migrations)
- [From v1.x to v2.x](#from-v1x-to-v2x)
- [From v2.x to v3.x](#from-v2x-to-v3x)
Expand Down Expand Up @@ -1077,6 +1078,20 @@ it("dispatches the correct message", async () => {

This works for function components using the `useElmish` hook and class components.

## Redux Dev Tools

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.

```ts
import { init } from "react-elmish";

init({
enableDevTools: true,
});
```

Hint: You should only enable this in development mode.

## Migrations

### From v1.x to v2.x
Expand Down
4 changes: 4 additions & 0 deletions src/Init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ interface ElmOptions {
* @type {DispatchMiddlewareFunc}
*/
dispatchMiddleware?: DispatchMiddlewareFunc;

enableDevTools?: boolean;
}

const Services: ElmOptions = {
logger: undefined,
errorMiddleware: undefined,
dispatchMiddleware: undefined,
enableDevTools: false,
};

/**
Expand All @@ -44,6 +47,7 @@ function init(options: ElmOptions): void {
Services.logger = options.logger;
Services.errorMiddleware = options.errorMiddleware;
Services.dispatchMiddleware = options.dispatchMiddleware;
Services.enableDevTools = options.enableDevTools;
}

export type { DispatchMiddlewareFunc, ElmOptions, ErrorMiddlewareFunc, Logger };
Expand Down
46 changes: 46 additions & 0 deletions src/reduxDevTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
interface ReduxDevToolsExtension {
connect: (options?: ReduxOptions) => ReduxDevTools;
disconnect: () => void;
}

interface ReduxOptions {
name?: string;
serialize?: {
options?: boolean;
};
}

interface ReduxDevTools {
init: (state: unknown) => void;
send: (message: string, state: unknown, options?: ReduxOptions) => void;
subscribe: (callback: (message: ReduxMessage) => void) => () => void;
unsubscribe: () => void;
}

interface ReduxMessage {
type: string;
payload: {
type: string;
};
state: string;
}

interface ReduxDevToolsExtensionWindow extends Window {
// biome-ignore lint/style/useNamingConvention: Predefined
__REDUX_DEVTOOLS_EXTENSION__: ReduxDevToolsExtension;
}

declare global {
interface Window {
// biome-ignore lint/style/useNamingConvention: Predefined
__REDUX_DEVTOOLS_EXTENSION__?: ReduxDevToolsExtension;
}
}

function isReduxDevToolsEnabled(window: Window | undefined): window is ReduxDevToolsExtensionWindow {
return window !== undefined && "__REDUX_DEVTOOLS_EXTENSION__" in window;
}

export type { ReduxDevTools, ReduxDevToolsExtension };

export { isReduxDevToolsEnabled };
27 changes: 26 additions & 1 deletion src/useElmish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
import { createCallBase } from "./createCallBase";
import { createDefer } from "./createDefer";
import { getFakeOptionsOnce } from "./fakeOptions";
import { isReduxDevToolsEnabled, type ReduxDevTools } from "./reduxDevTools";

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

const devTools = useRef<ReduxDevTools | null>(null);

useEffect(() => {
let reduxUnsubscribe: (() => void) | undefined;

if (Services.enableDevTools === true && isReduxDevToolsEnabled(window)) {
// eslint-disable-next-line no-underscore-dangle
devTools.current = window.__REDUX_DEVTOOLS_EXTENSION__.connect({ name, serialize: { options: true } });

reduxUnsubscribe = devTools.current.subscribe((message) => {
if (message.type === "DISPATCH" && message.payload.type === "JUMP_TO_ACTION") {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
setModel(JSON.parse(message.state) as TModel);
}
});
}

isMountedRef.current = true;

return () => {
isMountedRef.current = false;

reduxUnsubscribe?.();
};
}, []);
}, [name]);

let initializedModel = model;

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

if (devTools.current) {
devTools.current.send(nextMsg.name, { ...initializedModel, ...currentModel });
}

nextMsg = buffer.shift();
} while (nextMsg);

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

devTools.current?.init(initializedModel);

Services.logger?.debug("Initial model for", name, initializedModel);

execCmd(dispatch, ...initCommands);
Expand Down