Skip to content

Commit 74c2b38

Browse files
committed
refactor: use an options object for the useElmish hook
BREAKING CHANGE: * the signature of the `useElmish` hook has changed * useElmish with multiple parameters is moved and marked as deprecated * useElmishMap is moved and marked as deprecated
1 parent 0f0a3fc commit 74c2b38

File tree

8 files changed

+425
-182
lines changed

8 files changed

+425
-182
lines changed

README.md

Lines changed: 265 additions & 155 deletions
Large diffs are not rendered by default.

src/Testing/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { MessageBase, Nullable, UpdateMap } from "../ElmUtilities";
2-
import { callUpdateMap } from "../useElmishMap";
2+
import { callUpdateMap } from "../useElmish";
33
import { Cmd } from "../Cmd";
44
import { UpdateReturnType } from "../ElmComponent";
55

src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { ElmComponent, UpdateReturnType } from "./ElmComponent";
33
import { ErrorMessage, handleError, MsgSource, UpdateMap } from "./ElmUtilities";
44
import { init, Logger, Message } from "./Init";
55
import { useElmish } from "./useElmish";
6-
import { useElmishMap } from "./useElmishMap";
76

87
export type {
98
Logger,
@@ -22,5 +21,4 @@ export {
2221
ElmComponent,
2322
handleError,
2423
useElmish,
25-
useElmishMap,
2624
};

src/useElmishMap.ts renamed to src/legacy/useElmish.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
import { Cmd, Dispatch } from "./Cmd";
2-
import { dispatchMiddleware, LoggerService } from "./Init";
3-
import { MessageBase, Nullable, UpdateMap } from "./ElmUtilities";
1+
import { Cmd, Dispatch } from "../Cmd";
2+
import { dispatchMiddleware, LoggerService } from "../Init";
3+
import { MessageBase, Nullable } from "../ElmUtilities";
44
import { useCallback, useState } from "react";
5-
import { UpdateReturnType } from "./ElmComponent";
6-
7-
export function useElmishMap<TProps, TModel, TMsg extends MessageBase> (props: TProps, init: (props: TProps) => [TModel, Cmd<TMsg>], updateMap: UpdateMap<TProps, TModel, TMsg>, name: string): [TModel, Dispatch<TMsg>] {
5+
import { UpdateFunction } from "../ElmComponent";
6+
7+
/**
8+
* Hook to use the Elm architecture pattern in a function component.
9+
* @param props The props of the component.
10+
* @param init Function to initialize the model.
11+
* @param update The update function.
12+
* @param name The name of the component.
13+
* @returns A tuple containing the current model and the dispatcher.
14+
* @example
15+
* const [model, dispatch] = useElmish(props, init, update, "MyComponent");
16+
* @deprecated Use `useElmish` with an options object instead.
17+
*/
18+
export function useElmish<TProps, TModel, TMsg extends MessageBase> (props: TProps, init: (props: TProps) => [TModel, Cmd<TMsg>], update: UpdateFunction<TProps, TModel, TMsg>, name: string): [TModel, Dispatch<TMsg>] {
819
let reentered = false;
920
const buffer: TMsg [] = [];
1021
let currentModel: Partial<TModel> = {};
@@ -46,7 +57,7 @@ export function useElmishMap<TProps, TModel, TMsg extends MessageBase> (props: T
4657
LoggerService?.debug("Elm", "message from", name, nextMsg);
4758

4859
try {
49-
const [newModel, cmd] = callUpdateMap(updateMap, nextMsg, { ...initializedModel, ...currentModel }, props);
60+
const [newModel, cmd] = update({ ...initializedModel, ...currentModel }, nextMsg, props);
5061

5162
if (modelHasChanged(newModel)) {
5263
currentModel = { ...currentModel, ...newModel };
@@ -87,10 +98,4 @@ export function useElmishMap<TProps, TModel, TMsg extends MessageBase> (props: T
8798
}
8899

89100
return [initializedModel, dispatch];
90-
}
91-
92-
export function callUpdateMap<TProps, TModel, TMessage extends MessageBase> (updateMap: UpdateMap<TProps, TModel, TMessage>, msg: TMessage, model: TModel, props: TProps): UpdateReturnType<TModel, TMessage> {
93-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
94-
// @ts-expect-error -- We know that nextMsg fits
95-
return updateMap[msg.name as TMessage["name"]](msg, model, props);
96101
}

src/legacy/useElmishMap.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { Cmd, Dispatch } from "../Cmd";
2+
import { dispatchMiddleware, LoggerService } from "../Init";
3+
import { MessageBase, Nullable, UpdateMap } from "../ElmUtilities";
4+
import { useCallback, useState } from "react";
5+
import { callUpdateMap } from "../useElmish";
6+
7+
/**
8+
* Hook to use the Elm architecture pattern in a function component.
9+
* @param props The props of the component.
10+
* @param init Function to initialize the model.
11+
* @param updateMap The update map object.
12+
* @param name The name of the component.
13+
* @returns A tuple containing the current model and the dispatcher.
14+
* @example
15+
* const [model, dispatch] = useElmishMap(props, init, updateMap, "MyComponent");
16+
* @deprecated Use `useElmish` with an options object instead.
17+
*/
18+
export function useElmishMap<TProps, TModel, TMessage extends MessageBase> (props: TProps, init: (props: TProps) => [TModel, Cmd<TMessage>], updateMap: UpdateMap<TProps, TModel, TMessage>, name: string): [TModel, Dispatch<TMessage>] {
19+
let reentered = false;
20+
const buffer: TMessage [] = [];
21+
let currentModel: Partial<TModel> = {};
22+
23+
const [model, setModel] = useState<Nullable<TModel>>(null);
24+
let initializedModel = model;
25+
26+
const execCmd = useCallback((cmd: Cmd<TMessage>): void => {
27+
cmd.forEach(call => {
28+
try {
29+
call(dispatch);
30+
} catch (ex: unknown) {
31+
LoggerService?.error(ex);
32+
}
33+
});
34+
}, []);
35+
36+
const dispatch = useCallback((msg: TMessage): void => {
37+
if (!initializedModel) {
38+
return;
39+
}
40+
41+
const modelHasChanged = (updatedModel: Partial<TModel>): boolean => updatedModel !== initializedModel && Object.getOwnPropertyNames(updatedModel).length > 0;
42+
43+
if (dispatchMiddleware) {
44+
dispatchMiddleware(msg);
45+
}
46+
47+
if (reentered) {
48+
buffer.push(msg);
49+
} else {
50+
reentered = true;
51+
52+
let nextMsg: TMessage | undefined = msg;
53+
let modified = false;
54+
55+
while (nextMsg) {
56+
LoggerService?.info("Elm", "message from", name, nextMsg.name);
57+
LoggerService?.debug("Elm", "message from", name, nextMsg);
58+
59+
try {
60+
const [newModel, cmd] = callUpdateMap(updateMap, nextMsg, { ...initializedModel, ...currentModel }, props);
61+
62+
if (modelHasChanged(newModel)) {
63+
currentModel = { ...currentModel, ...newModel };
64+
65+
modified = true;
66+
}
67+
68+
if (cmd) {
69+
execCmd(cmd);
70+
}
71+
} catch (ex: unknown) {
72+
LoggerService?.error(ex);
73+
}
74+
75+
nextMsg = buffer.shift();
76+
}
77+
reentered = false;
78+
79+
if (modified) {
80+
setModel(prevModel => {
81+
const updatedModel = { ...prevModel as TModel, ...currentModel };
82+
83+
LoggerService?.debug("Elm", "update model for", name, updatedModel);
84+
85+
return updatedModel;
86+
});
87+
}
88+
}
89+
}, []);
90+
91+
if (!initializedModel) {
92+
const [initModel, initCmd] = init(props);
93+
94+
initializedModel = initModel;
95+
setModel(initializedModel);
96+
97+
execCmd(initCmd);
98+
}
99+
100+
return [initializedModel, dispatch];
101+
}

src/useElmish.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
11
import { Cmd, Dispatch } from "./Cmd";
22
import { dispatchMiddleware, LoggerService } from "./Init";
3-
import { MessageBase, Nullable } from "./ElmUtilities";
3+
import { MessageBase, Nullable, UpdateMap } from "./ElmUtilities";
4+
import { UpdateFunction, UpdateReturnType } from "./ElmComponent";
45
import { useCallback, useState } from "react";
5-
import { UpdateFunction } from "./ElmComponent";
66

7-
export function useElmish<TProps, TModel, TMsg extends MessageBase> (props: TProps, init: (props: TProps) => [TModel, Cmd<TMsg>], update: UpdateFunction<TProps, TModel, TMsg>, name: string): [TModel, Dispatch<TMsg>] {
7+
interface UseElmishOptions<TProps, TModel, TMessage extends MessageBase> {
8+
props: TProps,
9+
init: (props: TProps) => [TModel, Cmd<TMessage>],
10+
update: UpdateFunction<TProps, TModel, TMessage> | UpdateMap<TProps, TModel, TMessage>,
11+
name: string,
12+
}
13+
14+
/**
15+
* Hook to use the Elm architecture pattern in a function component.
16+
* @param {UseElmishOptions} options The options passed the the hook.
17+
* @returns A tuple containing the current model and the dispatcher.
18+
* @example
19+
* const [model, dispatch] = useElmish({ props, init, update, name: "MyComponent" });
20+
*/
21+
export function useElmish<TProps, TModel, TMessage extends MessageBase> ({ props, init, update, name }: UseElmishOptions<TProps, TModel, TMessage>): [TModel, Dispatch<TMessage>] {
822
let reentered = false;
9-
const buffer: TMsg [] = [];
23+
const buffer: TMessage [] = [];
1024
let currentModel: Partial<TModel> = {};
1125

1226
const [model, setModel] = useState<Nullable<TModel>>(null);
1327
let initializedModel = model;
1428

15-
const execCmd = useCallback((cmd: Cmd<TMsg>): void => {
29+
const execCmd = useCallback((cmd: Cmd<TMessage>): void => {
1630
cmd.forEach(call => {
1731
try {
1832
call(dispatch);
@@ -22,7 +36,7 @@ export function useElmish<TProps, TModel, TMsg extends MessageBase> (props: TPro
2236
});
2337
}, []);
2438

25-
const dispatch = useCallback((msg: TMsg): void => {
39+
const dispatch = useCallback((msg: TMessage): void => {
2640
if (!initializedModel) {
2741
return;
2842
}
@@ -38,15 +52,15 @@ export function useElmish<TProps, TModel, TMsg extends MessageBase> (props: TPro
3852
} else {
3953
reentered = true;
4054

41-
let nextMsg: TMsg | undefined = msg;
55+
let nextMsg: TMessage | undefined = msg;
4256
let modified = false;
4357

4458
while (nextMsg) {
4559
LoggerService?.info("Elm", "message from", name, nextMsg.name);
4660
LoggerService?.debug("Elm", "message from", name, nextMsg);
4761

4862
try {
49-
const [newModel, cmd] = update({ ...initializedModel, ...currentModel }, nextMsg, props);
63+
const [newModel, cmd] = callUpdate(update, nextMsg, { ...initializedModel, ...currentModel }, props);
5064

5165
if (modelHasChanged(newModel)) {
5266
currentModel = { ...currentModel, ...newModel };
@@ -87,4 +101,18 @@ export function useElmish<TProps, TModel, TMsg extends MessageBase> (props: TPro
87101
}
88102

89103
return [initializedModel, dispatch];
104+
}
105+
106+
export function callUpdate<TProps, TModel, TMessage extends MessageBase> (update: UpdateFunction<TProps, TModel, TMessage> | UpdateMap<TProps, TModel, TMessage>, msg: TMessage, model: TModel, props: TProps): UpdateReturnType<TModel, TMessage> {
107+
if (typeof update === "function") {
108+
return update(model, msg, props);
109+
}
110+
111+
return callUpdateMap(update, msg, model, props);
112+
}
113+
114+
export function callUpdateMap<TProps, TModel, TMessage extends MessageBase> (updateMap: UpdateMap<TProps, TModel, TMessage>, msg: TMessage, model: TModel, props: TProps): UpdateReturnType<TModel, TMessage> {
115+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
116+
// @ts-expect-error -- We know that nextMsg fits
117+
return updateMap[msg.name as TMessage["name"]](msg, model, props);
90118
}

tests/useElmish.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ describe("Hooks", () => {
132132

133133
function TestComponent (props: Props): JSX.Element {
134134
const { init, update } = props;
135-
const [model] = useElmish(props, init, update, "Test");
135+
const [model] = useElmish({ props, init, update, name: "Test" });
136136

137137
componentModel = model;
138138

@@ -147,7 +147,7 @@ function renderComponent (props: Props): RenderResult {
147147

148148
function TestComponentWithEffect (props: Props): JSX.Element {
149149
const { init, update } = props;
150-
const [model, dispatch] = useElmish(props, init, update, "Test");
150+
const [model, dispatch] = useElmish({ props, init, update, name: "Test" });
151151

152152
if (model.value1 === "") {
153153
setTimeout(() => dispatch({ name: "First" }), 5);

tests/useElmishMap.spec.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Cmd, createCmd, UpdateMap, useElmishMap } from "../src";
1+
import { Cmd, createCmd, UpdateMap } from "../src";
22
import { render, RenderResult, waitFor } from "@testing-library/react";
33
import { useEffect } from "react";
4+
import { useElmishMap } from "../src/legacy/useElmishMap";
45

56
type Message =
67
| { name: "Test" }

0 commit comments

Comments
 (0)