react-elmish 2.2.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +424 -210
- package/dist/Cmd.d.ts +5 -0
- package/dist/Cmd.js +12 -1
- package/dist/ElmComponent.d.ts +4 -2
- package/dist/ElmComponent.js +1 -1
- package/dist/ElmUtilities.d.ts +3 -0
- package/dist/ElmUtilities.js +11 -2
- package/dist/Testing/index.js +3 -3
- package/dist/index.d.ts +5 -6
- package/dist/index.js +7 -9
- package/dist/legacy/useElmish.d.ts +15 -0
- package/dist/legacy/useElmish.js +135 -0
- package/dist/legacy/useElmishMap.d.ts +15 -0
- package/dist/legacy/useElmishMap.js +137 -0
- package/dist/useElmish.d.ts +22 -3
- package/dist/useElmish.js +52 -7
- package/package.json +2 -2
- package/dist/useElmishMap.d.ts +0 -5
- package/dist/useElmishMap.js +0 -128
package/README.md
CHANGED
|
@@ -25,66 +25,75 @@ An elmish component basically consists of the following parts:
|
|
|
25
25
|
First import everything from `react-elmish` and declare the **Message** discriminated union type:
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
|
-
import
|
|
28
|
+
import { Cmd, createCmd, InitResult, UpdateReturnType, UpdateMap } from "react-elmish";
|
|
29
29
|
|
|
30
30
|
export type Message =
|
|
31
|
-
| { name: "
|
|
32
|
-
| { name: "
|
|
33
|
-
;
|
|
31
|
+
| { name: "increment" }
|
|
32
|
+
| { name: "decrement" };
|
|
34
33
|
```
|
|
35
34
|
|
|
36
|
-
You can also create some convenience functions to
|
|
35
|
+
You can also create some convenience functions to create message objects:
|
|
37
36
|
|
|
38
37
|
```ts
|
|
39
38
|
export const Msg = {
|
|
40
|
-
increment: (): Message => ({ name: "
|
|
41
|
-
decrement: (): Message => ({ name: "
|
|
39
|
+
increment: (): Message => ({ name: "increment" }),
|
|
40
|
+
decrement: (): Message => ({ name: "decrement" }),
|
|
42
41
|
};
|
|
43
42
|
```
|
|
44
43
|
|
|
45
|
-
Now we can create a `cmd` object for our messages type:
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
const cmd = Elm.createCmd<Message>();
|
|
49
|
-
```
|
|
50
|
-
|
|
51
44
|
Next, declare the model:
|
|
52
45
|
|
|
53
46
|
```ts
|
|
54
|
-
export
|
|
47
|
+
export interface Model {
|
|
55
48
|
value: number,
|
|
56
|
-
}
|
|
49
|
+
}
|
|
57
50
|
```
|
|
58
51
|
|
|
59
52
|
The props are optional:
|
|
60
53
|
|
|
61
54
|
```ts
|
|
62
|
-
export
|
|
55
|
+
export interface Props {
|
|
63
56
|
initialValue: number,
|
|
64
|
-
}
|
|
57
|
+
}
|
|
65
58
|
```
|
|
66
59
|
|
|
67
60
|
To create the initial model we need an **init** function:
|
|
68
61
|
|
|
69
62
|
```ts
|
|
70
|
-
export
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
export function init (props: Props): InitResult {
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
value: props.initialValue,
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
To update the model based on a message we need an `UpdateMap` object:
|
|
74
73
|
|
|
75
|
-
|
|
74
|
+
```ts
|
|
75
|
+
export const update: UpdateMap<Props, Model, Message> = {
|
|
76
|
+
increment (msg, model, props) {
|
|
77
|
+
return [{ value: model.value + 1 }];
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
decrement (msg, model, props) {
|
|
81
|
+
return [{ value: model.value - 1 }];
|
|
82
|
+
},
|
|
76
83
|
};
|
|
77
84
|
```
|
|
78
85
|
|
|
79
|
-
|
|
86
|
+
**Note:** When using an `UpdateMap` it is recommended to use camelCase for message names ("increment" instead of "Increment").
|
|
87
|
+
|
|
88
|
+
Alternatively we can use an **update** function:
|
|
80
89
|
|
|
81
90
|
```ts
|
|
82
|
-
export const update = (model: Model, msg: Msg, props: Props):
|
|
91
|
+
export const update = (model: Model, msg: Msg, props: Props): UpdateReturnType<Model, Message> => {
|
|
83
92
|
switch (msg.name) {
|
|
84
|
-
case "
|
|
93
|
+
case "increment":
|
|
85
94
|
return [{ value: model.value + 1 }];
|
|
86
95
|
|
|
87
|
-
case "
|
|
96
|
+
case "decrement":
|
|
88
97
|
return [{ value: model.value - 1 }];
|
|
89
98
|
}
|
|
90
99
|
};
|
|
@@ -96,25 +105,49 @@ export const update = (model: Model, msg: Msg, props: Props): Elm.UpdateReturnTy
|
|
|
96
105
|
|
|
97
106
|
To put all this together and to render our component, we need a React component.
|
|
98
107
|
|
|
99
|
-
|
|
108
|
+
As a **function component**:
|
|
100
109
|
|
|
101
110
|
```tsx
|
|
102
111
|
// Import everything from the App.ts
|
|
103
|
-
import
|
|
112
|
+
import { init, update, Msg, Props } from "../App";
|
|
113
|
+
// Import the useElmish hook
|
|
114
|
+
import { useElmish } from "react-elmish";
|
|
115
|
+
|
|
116
|
+
function App (props: Props): JSX.Element {
|
|
117
|
+
// Call the useElmish hook, it returns the current model and the dispatch function
|
|
118
|
+
const [model, dispatch] = useElmish({ props, init, update, name: "App" });
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div>
|
|
122
|
+
{/* Display our current value */}
|
|
123
|
+
<p>{model.value}</p>
|
|
124
|
+
|
|
125
|
+
{/* dispatch messages */}
|
|
126
|
+
<button onClick={() => dispatch(Msg.increment())}>Increment</button>
|
|
127
|
+
<button onClick={() => dispatch(Msg.decrement())}>Decrement</button>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
As a **class component**:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
// Import everything from the App.ts
|
|
137
|
+
import { Model, Message, Props, init, update, Msg } as Shared from "../App";
|
|
104
138
|
// Import the ElmComponent which extends the React.Component
|
|
105
139
|
import { ElmComponent } from "react-elmish";
|
|
106
|
-
// Don't forget to import react
|
|
107
140
|
import React from "react";
|
|
108
141
|
|
|
109
142
|
// Create an elmish class component
|
|
110
|
-
class App extends ElmComponent<
|
|
143
|
+
class App extends ElmComponent<Model, Message, Props> {
|
|
111
144
|
// Construct the component with the props and init function
|
|
112
|
-
constructor(props:
|
|
113
|
-
super(props,
|
|
145
|
+
constructor(props: Props) {
|
|
146
|
+
super(props, init, "App");
|
|
114
147
|
}
|
|
115
148
|
|
|
116
149
|
// Assign our update function to the component
|
|
117
|
-
update =
|
|
150
|
+
update = update;
|
|
118
151
|
|
|
119
152
|
render(): React.ReactNode {
|
|
120
153
|
// Access the model
|
|
@@ -126,72 +159,19 @@ class App extends ElmComponent<Shared.Model, Shared.Message, Shared.Props> {
|
|
|
126
159
|
<p>{value}</p>
|
|
127
160
|
|
|
128
161
|
{/* Dispatch messages */}
|
|
129
|
-
<button onClick={() => this.dispatch(
|
|
130
|
-
<button onClick={() => this.dispatch(
|
|
162
|
+
<button onClick={() => this.dispatch(Msg.increment())}>Increment</button>
|
|
163
|
+
<button onClick={() => this.dispatch(Msg.decrement())}>Decrement</button>
|
|
131
164
|
</div>
|
|
132
165
|
);
|
|
133
166
|
}
|
|
134
167
|
```
|
|
135
168
|
|
|
136
|
-
Or it can be a **functional component**:
|
|
137
|
-
|
|
138
|
-
```tsx
|
|
139
|
-
// Import everything from the App.ts
|
|
140
|
-
import * as Shared from "../App";
|
|
141
|
-
// Import the useElmish hook
|
|
142
|
-
import { useElmish } from "react-elmish";
|
|
143
|
-
|
|
144
|
-
const App = (props: Shared.Props) => {
|
|
145
|
-
// Call the useElmish hook, it returns the current model and the dispatch function
|
|
146
|
-
const [model, dispatch] = useElmish(props, Shared.init, Shared.update, "App");
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<div>
|
|
150
|
-
{/* Display our current value */}
|
|
151
|
-
<p>{model.value}</p>
|
|
152
|
-
|
|
153
|
-
{/* dispatch messages */}
|
|
154
|
-
<button onClick={() => dispatch(Shared.Msg.increment())}>Increment</button>
|
|
155
|
-
<button onClick={() => dispatch(Shared.Msg.decrement())}>Decrement</button>
|
|
156
|
-
</div>
|
|
157
|
-
);
|
|
158
|
-
};
|
|
159
|
-
```
|
|
160
|
-
|
|
161
169
|
You can use these components like any other React component.
|
|
162
170
|
|
|
163
171
|
> **Note**: It is recommended to separate business logic and the view into separate modules. Here we put the `Messages`, `Model`, `Props`, `init`, and `update` functions into **App.ts**. The elmish React Component resides in a **Components** subfolder and is named **App.tsx**.
|
|
164
172
|
>
|
|
165
173
|
> You can even split the contents of the **App.ts** into two files: **Types.ts** (`Message`, `Model`, and `Props`) and **State.ts** (`init` and `update`).
|
|
166
174
|
|
|
167
|
-
## A new approach
|
|
168
|
-
|
|
169
|
-
Instead of `useElmish` you can use the `useElmishMap` hook. Then you have an `UpdateMap` instead of an `update` function:
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
const updateMap: UpdateMap<Props, Model, Message> {
|
|
173
|
-
// Now the message is the first parameter, so it is easier to omit the model parameter.
|
|
174
|
-
Increment: (msg) => [{ value: model.value + 1 }],
|
|
175
|
-
Decrement: (msg) => [{ value: model.value - 1 }],
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Add your component looks like:
|
|
180
|
-
|
|
181
|
-
```tsx
|
|
182
|
-
import { useElmishMap } from "react-elmish";
|
|
183
|
-
|
|
184
|
-
const App = (props: Shared.Props) => {
|
|
185
|
-
const [model, dispatch] = useElmishMap(props, init, updateMap, "App");
|
|
186
|
-
|
|
187
|
-
return (
|
|
188
|
-
<div>
|
|
189
|
-
...
|
|
190
|
-
</div>
|
|
191
|
-
);
|
|
192
|
-
};
|
|
193
|
-
```
|
|
194
|
-
|
|
195
175
|
## More on messages
|
|
196
176
|
|
|
197
177
|
### Message arguments
|
|
@@ -200,11 +180,11 @@ Messages can also have arguments. You can modify the example above and pass an o
|
|
|
200
180
|
|
|
201
181
|
```ts
|
|
202
182
|
export type Message =
|
|
203
|
-
| { name: "
|
|
183
|
+
| { name: "increment", step?: number }
|
|
204
184
|
...
|
|
205
185
|
|
|
206
186
|
export const Msg = {
|
|
207
|
-
increment: (step?: number): Message => ({ name: "
|
|
187
|
+
increment: (step?: number): Message => ({ name: "increment", step }),
|
|
208
188
|
...
|
|
209
189
|
}
|
|
210
190
|
```
|
|
@@ -213,7 +193,7 @@ Then use this argument in the **update** function:
|
|
|
213
193
|
|
|
214
194
|
```ts
|
|
215
195
|
...
|
|
216
|
-
case "
|
|
196
|
+
case "increment":
|
|
217
197
|
return [{ value: model.value + (msg.step ?? 1)}]
|
|
218
198
|
...
|
|
219
199
|
```
|
|
@@ -264,74 +244,19 @@ You can also use **Symbols** for the message type instead of strings:
|
|
|
264
244
|
...
|
|
265
245
|
```
|
|
266
246
|
|
|
267
|
-
##
|
|
247
|
+
## Dispatch commands in the update map or update function
|
|
268
248
|
|
|
269
|
-
|
|
249
|
+
In addition to modifying the model, you can dispatch new commands here.
|
|
270
250
|
|
|
271
|
-
|
|
272
|
-
import * as Elm from "react-elmish";
|
|
251
|
+
To do so, you have to create a `cmd` object:
|
|
273
252
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
console.debug(...args);
|
|
277
|
-
},
|
|
278
|
-
info(...args: unknown []) {
|
|
279
|
-
console.info(...args);
|
|
280
|
-
},
|
|
281
|
-
error(...args: unknown []) {
|
|
282
|
-
console.error(...args);
|
|
283
|
-
},
|
|
284
|
-
}
|
|
253
|
+
```ts
|
|
254
|
+
import { createCmd } from "react-elmish";
|
|
285
255
|
|
|
286
|
-
|
|
287
|
-
logger: myLogger,
|
|
288
|
-
errorMiddleware: error => Toast.error(error.message),
|
|
289
|
-
dispatchMiddleware: msg => console.log(msg),
|
|
290
|
-
});
|
|
256
|
+
const cmd = createCmd<Message>();
|
|
291
257
|
```
|
|
292
258
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
The dispatch middleware function is called whenever a Message is dispatched.
|
|
296
|
-
|
|
297
|
-
## Error handling
|
|
298
|
-
|
|
299
|
-
You can handle errors easily with the following pattern.
|
|
300
|
-
|
|
301
|
-
1. Add an error message:
|
|
302
|
-
|
|
303
|
-
```ts
|
|
304
|
-
export type Message =
|
|
305
|
-
| ...
|
|
306
|
-
| { name: "Error", error: Error }
|
|
307
|
-
;
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
1. Optionally add the convenient function to the **Msg** object:
|
|
311
|
-
|
|
312
|
-
```ts
|
|
313
|
-
export const Msg = {
|
|
314
|
-
...
|
|
315
|
-
error: (error: Error): Message => ({ name: "Error", error }),
|
|
316
|
-
}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
1. Handle the error message in the **update** function:
|
|
320
|
-
|
|
321
|
-
```ts
|
|
322
|
-
...
|
|
323
|
-
case "Error":
|
|
324
|
-
return Elm.handleError(msg.error);
|
|
325
|
-
...
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
The **handleError** function then calls your error handling middleware.
|
|
329
|
-
|
|
330
|
-
## Dispatch commands in the update function
|
|
331
|
-
|
|
332
|
-
In addition to modifying the model, you can dispatch new commands in the **update** function.
|
|
333
|
-
|
|
334
|
-
To do so, you can call one of the functions in the `cmd` object:
|
|
259
|
+
Then you can call one of the functions of that object:
|
|
335
260
|
|
|
336
261
|
| Function | Description |
|
|
337
262
|
|---|---|
|
|
@@ -344,6 +269,7 @@ To do so, you can call one of the functions in the `cmd` object:
|
|
|
344
269
|
| `cmd.ofPromise.either` | Calls an async function and maps the result into a message. |
|
|
345
270
|
| `cmd.ofPromise.attempt` | Like `either` but ignores the success case. |
|
|
346
271
|
| `cmd.ofPromise.perform` | Like `either` but ignores the error case. |
|
|
272
|
+
| `cmd.ofSub` | Use this function to trigger a command in a subscription. |
|
|
347
273
|
|
|
348
274
|
### Dispatch a message
|
|
349
275
|
|
|
@@ -352,24 +278,26 @@ Let's assume you have a message to display the description of the last called me
|
|
|
352
278
|
```ts
|
|
353
279
|
export type Message =
|
|
354
280
|
...
|
|
355
|
-
| { name: "
|
|
281
|
+
| { name: "printLastMessage", message: string }
|
|
356
282
|
...
|
|
357
283
|
|
|
358
284
|
export const Msg = {
|
|
359
285
|
...
|
|
360
|
-
printLastMessage: (message: string): Message => ({ name: "
|
|
286
|
+
printLastMessage: (message: string): Message => ({ name: "printLastMessage", message }),
|
|
361
287
|
...
|
|
362
288
|
}
|
|
289
|
+
|
|
290
|
+
const cmd = createCmd<Message>();
|
|
363
291
|
```
|
|
364
292
|
|
|
365
293
|
In the **update** function you can dispatch that message like this:
|
|
366
294
|
|
|
367
295
|
```ts
|
|
368
|
-
case "
|
|
296
|
+
case "increment":
|
|
369
297
|
return [{ value: model.value + 1 }, cmd.ofMsg(Msg.printLastMessage("Incremented by one"))];
|
|
370
298
|
```
|
|
371
299
|
|
|
372
|
-
This new message will immediately be dispatched after returning from the **update
|
|
300
|
+
This new message will immediately be dispatched after returning from the **update**.
|
|
373
301
|
|
|
374
302
|
### Call an async function
|
|
375
303
|
|
|
@@ -387,16 +315,16 @@ you can define the following messages:
|
|
|
387
315
|
```ts
|
|
388
316
|
export type Messages =
|
|
389
317
|
...
|
|
390
|
-
| { name: "
|
|
391
|
-
| { name: "
|
|
392
|
-
|
|
|
318
|
+
| { name: "loadSettings" },
|
|
319
|
+
| { name: "settingsLoaded", settings: Settings }
|
|
320
|
+
| ErrorMessage
|
|
393
321
|
...
|
|
394
322
|
|
|
395
323
|
export const Msg = {
|
|
396
324
|
...
|
|
397
|
-
loadSettings: (): Message => ({ name: "
|
|
398
|
-
settingsLoaded: (settings: Settings): Message => ({ name: "
|
|
399
|
-
|
|
325
|
+
loadSettings: (): Message => ({ name: "loadSettings" }),
|
|
326
|
+
settingsLoaded: (settings: Settings): Message => ({ name: "settingsLoaded", settings }),
|
|
327
|
+
...errorMsg,
|
|
400
328
|
...
|
|
401
329
|
};
|
|
402
330
|
```
|
|
@@ -405,21 +333,185 @@ and handle the messages in the **update** function:
|
|
|
405
333
|
|
|
406
334
|
```ts
|
|
407
335
|
...
|
|
408
|
-
case "
|
|
336
|
+
case "loadSettings":
|
|
409
337
|
// Create a command out of the async function with the provided arguments
|
|
410
338
|
// If loadSettings resolves it dispatches "SettingsLoaded"
|
|
411
339
|
// If it fails it dispatches "Error"
|
|
412
340
|
// The return type of loadSettings must fit Msg.settingsLoaded
|
|
413
341
|
return [{}, cmd.ofPromise.either(loadSettings, Msg.settingsLoaded, Msg.error, "firstArg", 123)];
|
|
414
342
|
|
|
415
|
-
case "
|
|
343
|
+
case "settingsLoaded":
|
|
416
344
|
return [{ settings: msg.settings }];
|
|
417
345
|
|
|
418
|
-
case "
|
|
419
|
-
return
|
|
346
|
+
case "error":
|
|
347
|
+
return handleError(msg.error);
|
|
420
348
|
...
|
|
421
349
|
```
|
|
422
350
|
|
|
351
|
+
### Dispatch a command from `init`
|
|
352
|
+
|
|
353
|
+
The same way as in the `update` map or function, you can also dispatch an initial command in the `init` function:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
export function init (props: Props): InitResult {
|
|
357
|
+
return [
|
|
358
|
+
{
|
|
359
|
+
value: props.initialValue,
|
|
360
|
+
},
|
|
361
|
+
cmd.ofMsg(Msg.loadData())
|
|
362
|
+
];
|
|
363
|
+
};
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Subscriptions
|
|
367
|
+
|
|
368
|
+
### Working with external sources of events
|
|
369
|
+
|
|
370
|
+
If you want to use external sources of events (e.g. a timer), you can use a `subscription`. With this those events can be processed by our `update` handler.
|
|
371
|
+
|
|
372
|
+
Let's define a `Model` and a `Message`:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
type Message =
|
|
376
|
+
| { name: "timer", date: Date };
|
|
377
|
+
|
|
378
|
+
interface Model {
|
|
379
|
+
date: Date,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const Msg = {
|
|
383
|
+
timer: (date: Date): Message => ({ name: "timer", date }),
|
|
384
|
+
};
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Now we define the `init` function and the `update` object:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
const cmd = createCmd<Message>();
|
|
391
|
+
|
|
392
|
+
function init (props: Props): InitResult<Model, Message> {
|
|
393
|
+
return [{
|
|
394
|
+
date: new Date(),
|
|
395
|
+
}];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const update: UpdateMap<Props, Model, Message> = {
|
|
399
|
+
timer ({ date }) {
|
|
400
|
+
return [{ date }];
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Then we write our `subscription` function:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
function subscription (model: Model): SubscriptionResult<Message> {
|
|
409
|
+
const sub = (dispatch: Dispatch<Message>): void => {
|
|
410
|
+
setInterval(() => dispatch(Msg.timer(new Date())), 1000) as unknown as number;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return [cmd.ofSub(sub)];
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
This function gets the initialized model as parameter and returns a command.
|
|
418
|
+
|
|
419
|
+
In the function component we call `useElmish` and pass the subscription to it:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
const [{ date }] = useElmish({ name: "Subscriptions", props, init, update, subscription })
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
You can define and aggregate multiple subscriptions with a call to `cmd.batch(...)`.
|
|
426
|
+
|
|
427
|
+
### Cleanup subscriptions
|
|
428
|
+
|
|
429
|
+
In the solution above `setInterval` will trigger events even if the component is removed from the DOM. To cleanup subscriptions, we can return a `destructor` function from the subscription the same as in the `useEffect` hook.
|
|
430
|
+
|
|
431
|
+
Let's rewrite our `subscription` function:
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
function subscription (model: Model): SubscriptionResult<Message> {
|
|
435
|
+
let timer: NodeJS.Timer;
|
|
436
|
+
|
|
437
|
+
const sub = (dispatch: Dispatch<Message>): void => {
|
|
438
|
+
timer = setInterval(() => dispatch(Msg.timer(new Date())), 1000);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const destructor = () => {
|
|
442
|
+
clearInterval(timer1);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return [cmd.ofSub(sub), destructor];
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Here we save the return value of `setInterval` and clear that interval in the returned `destructor` function.
|
|
450
|
+
|
|
451
|
+
## Setup
|
|
452
|
+
|
|
453
|
+
**react-elmish** works without a setup. But if you want to use logging or some middleware, you can setup **react-elmish** at the start of your program.
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
import { init } from "react-elmish";
|
|
457
|
+
|
|
458
|
+
const myLogger = {
|
|
459
|
+
debug(...args: unknown []) {
|
|
460
|
+
console.debug(...args);
|
|
461
|
+
},
|
|
462
|
+
info(...args: unknown []) {
|
|
463
|
+
console.info(...args);
|
|
464
|
+
},
|
|
465
|
+
error(...args: unknown []) {
|
|
466
|
+
console.error(...args);
|
|
467
|
+
},
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
init({
|
|
471
|
+
logger: myLogger,
|
|
472
|
+
errorMiddleware: error => Toast.error(error.message),
|
|
473
|
+
dispatchMiddleware: msg => myLogger.debug(msg),
|
|
474
|
+
});
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
The error middleware function is called by the `handleError` function (see [Error handling](#error-handling)).
|
|
478
|
+
|
|
479
|
+
The dispatch middleware function is called whenever a Message is dispatched.
|
|
480
|
+
|
|
481
|
+
## Error handling
|
|
482
|
+
|
|
483
|
+
You can handle errors easily with the following pattern.
|
|
484
|
+
|
|
485
|
+
1. Add an error message:
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
import { ErrorMessage, errorMsg, handleError } from "react-elmish";
|
|
489
|
+
|
|
490
|
+
export type Message =
|
|
491
|
+
| ...
|
|
492
|
+
| ErrorMessage;
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
1. Optionally add the convenient function to the **Msg** object:
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
export const Msg = {
|
|
499
|
+
...
|
|
500
|
+
...errorMsg,
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
1. Handle the error message in the **update** function:
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
...
|
|
508
|
+
case "error":
|
|
509
|
+
return handleError(msg.error);
|
|
510
|
+
...
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
The **handleError** function then calls your error handling middleware.
|
|
514
|
+
|
|
423
515
|
## React life cycle management
|
|
424
516
|
|
|
425
517
|
If you want to use `componentDidMount` or `componentWillUnmount` in a class component, don't forget to call the base class implementation of it as the **ElmComponent** is using them internally.
|
|
@@ -448,34 +540,141 @@ In a functional component you can use the **useEffect** hook as normal.
|
|
|
448
540
|
|
|
449
541
|
If you have some business logic that you want to reuse in other components, you can do this by using different sources for messages.
|
|
450
542
|
|
|
543
|
+
### With an `UpdateMap`
|
|
544
|
+
|
|
451
545
|
Let's say you want to load some settings, you can write a module like this:
|
|
452
546
|
|
|
453
547
|
```ts LoadSettings.ts
|
|
454
|
-
import
|
|
548
|
+
import { createCmd, Cmd, ErrorMessage, UpdateMap, handleError } from "react-elmish";
|
|
549
|
+
|
|
550
|
+
export interface Settings {
|
|
551
|
+
// ...
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export type Message =
|
|
555
|
+
| { name: "loadSettings" }
|
|
556
|
+
| { name: "settingsLoaded", settings: Settings }
|
|
557
|
+
| ErrorMessage;
|
|
558
|
+
|
|
559
|
+
export const Msg = {
|
|
560
|
+
loadSettings: (): Message => ({ name: "loadSettings" }),
|
|
561
|
+
settingsLoaded: (settings: Settings): Message => ({ name: "settingsLoaded", settings }),
|
|
562
|
+
error: (error: Error): Message => ({ name: "error", error }),
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const cmd = createCmd<Message>();
|
|
566
|
+
|
|
567
|
+
export interface Model {
|
|
568
|
+
settings: Settings | null,
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function init (): Model {
|
|
572
|
+
return {
|
|
573
|
+
settings: null
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export const update: UpdateMap<Props, Model, Message> = {
|
|
578
|
+
loadSettings () {
|
|
579
|
+
return [{}, cmd.ofPromise.either(loadSettings, Msg.settingsLoaded, Msg.error)];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
settingsLoaded ({ settings }) {
|
|
583
|
+
return [{ settings }];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
error ({ error }) {
|
|
587
|
+
return handleError(error);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
async function loadSettings (): Promise<Settings> {
|
|
592
|
+
// Call some service (e.g. database or backend)
|
|
593
|
+
return {};
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
> **Note**: This module has no **View**.
|
|
598
|
+
|
|
599
|
+
Now let's integrate the **LoadSettings** module in our component:
|
|
600
|
+
|
|
601
|
+
```ts Composition.ts
|
|
602
|
+
// Import the LoadSettings module
|
|
603
|
+
import * as LoadSettings from "./LoadSettings";
|
|
604
|
+
import { createCmd, Cmd, } from "react-elmish";
|
|
605
|
+
|
|
606
|
+
// Here we define our local messages
|
|
607
|
+
type Message =
|
|
608
|
+
| { name: "myMessage" }
|
|
609
|
+
| LoadSettings.Message;
|
|
610
|
+
|
|
611
|
+
// And spread the Msg of LoadSettings object
|
|
612
|
+
export const Msg = {
|
|
613
|
+
myMessage: (): Message => ({ name: "myMessage" }),
|
|
614
|
+
...LoadSettings.Msg,
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const cmd = Elm.createCmd<Message>();
|
|
618
|
+
|
|
619
|
+
interface Props {}
|
|
620
|
+
|
|
621
|
+
// Extend the LoadSettings model
|
|
622
|
+
interface Model extends LoadSettings.Model {
|
|
623
|
+
// ...
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export const init = (): [Model, Cmd<Message>] => {
|
|
627
|
+
// Return the model and dispatch the LoadSettings message
|
|
628
|
+
return [
|
|
629
|
+
{
|
|
630
|
+
// Spread the initial model from LoadSettings
|
|
631
|
+
...LoadSettings.init(),
|
|
632
|
+
// ...
|
|
633
|
+
},
|
|
634
|
+
cmd.ofMsg(Msg.loadSettings())
|
|
635
|
+
];
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// Spread the UpdateMap of LoadSettings into our update map
|
|
639
|
+
const update: UpdateMap<Props, Model, Message> = {
|
|
640
|
+
myMessage () {
|
|
641
|
+
return [{}];
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
...LoadSettings.update,
|
|
645
|
+
};
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
## With an update function
|
|
649
|
+
|
|
650
|
+
Let's say you want to load some settings, you can write a module like this:
|
|
651
|
+
|
|
652
|
+
```ts LoadSettings.ts
|
|
653
|
+
import { MsgSource, ErrorMessage, createCmd, UpdateReturnType, handleError } from "react-elmish";
|
|
455
654
|
|
|
456
655
|
export type Settings = {
|
|
457
656
|
// ...
|
|
458
657
|
};
|
|
459
658
|
|
|
460
659
|
// We use a MsgSource to differentiate between the messages
|
|
461
|
-
type MessageSource =
|
|
660
|
+
type MessageSource = MsgSource<"LoadSettings">;
|
|
462
661
|
|
|
463
662
|
// Add that MessageSource to all the messages
|
|
464
663
|
export type Message =
|
|
465
|
-
| { name: "
|
|
466
|
-
| { name: "
|
|
467
|
-
|
|
|
664
|
+
| { name: "loadSettings" } & MessageSource
|
|
665
|
+
| { name: "settingsLoaded", settings: Settings } & MessageSource
|
|
666
|
+
| ErrorMessage & MessageSource
|
|
468
667
|
|
|
469
668
|
// Do the same for the convenient functions
|
|
470
669
|
const MsgSource: MessageSource = { source: "LoadSettings" };
|
|
471
670
|
|
|
472
671
|
export const Msg = {
|
|
473
|
-
loadSettings: (): Message => ({ name: "
|
|
474
|
-
settingsLoaded: (settings: Settings): Message => ({ name: "
|
|
475
|
-
error: (error: Error): Message => ({ name: "
|
|
672
|
+
loadSettings: (): Message => ({ name: "loadSettings", ...MsgSource }),
|
|
673
|
+
settingsLoaded: (settings: Settings): Message => ({ name: "settingsLoaded", settings, ...MsgSource }),
|
|
674
|
+
error: (error: Error): Message => ({ name: "error", error, ...MsgSource }),
|
|
476
675
|
};
|
|
477
676
|
|
|
478
|
-
const cmd =
|
|
677
|
+
const cmd = createCmd<Message>();
|
|
479
678
|
|
|
480
679
|
export type Model = Readonly<{
|
|
481
680
|
settings: Settings | null,
|
|
@@ -485,23 +684,23 @@ export const init = (): Model => ({
|
|
|
485
684
|
settings: null,
|
|
486
685
|
});
|
|
487
686
|
|
|
488
|
-
export const update = (_model: Model, msg: Message):
|
|
687
|
+
export const update = (_model: Model, msg: Message): UpdateReturnType<Model, Message> => {
|
|
489
688
|
switch (msg.name) {
|
|
490
|
-
case "
|
|
689
|
+
case "loadSettings":
|
|
491
690
|
return [{}, cmd.ofPromise.either(loadSettings, Msg.settingsLoaded, Msg.error)];
|
|
492
691
|
|
|
493
|
-
case "
|
|
692
|
+
case "settingsLoaded":
|
|
494
693
|
return [{ settings: msg.settings }];
|
|
495
694
|
|
|
496
|
-
case "
|
|
497
|
-
return
|
|
695
|
+
case "error":
|
|
696
|
+
return handleError(msg.error);
|
|
498
697
|
}
|
|
499
698
|
};
|
|
500
699
|
|
|
501
|
-
|
|
700
|
+
async function loadSettings (): Promise<Settings> {
|
|
502
701
|
// Call some service (e.g. database or backend)
|
|
503
|
-
return
|
|
504
|
-
}
|
|
702
|
+
return {};
|
|
703
|
+
}
|
|
505
704
|
```
|
|
506
705
|
|
|
507
706
|
> **Note**: This module has no **View**.
|
|
@@ -509,51 +708,51 @@ const loadSettings = async (): Promise<Settings> => {
|
|
|
509
708
|
In other components where we want to use this **LoadSettings** module, we also need a message source:
|
|
510
709
|
|
|
511
710
|
```ts Composition.ts
|
|
512
|
-
import
|
|
711
|
+
import { createCmd, MsgSource, UpdateReturnType, Cmd } from "react-elmish";
|
|
513
712
|
// Import the LoadSettings module
|
|
514
713
|
import * as LoadSettings from "./LoadSettings";
|
|
515
714
|
|
|
516
715
|
// Create a message source for this module
|
|
517
|
-
type MessageSource =
|
|
716
|
+
type MessageSource = MsgSource<"Composition">;
|
|
518
717
|
|
|
519
718
|
// Here we define our local messages
|
|
520
719
|
// We don't need to export them
|
|
521
720
|
type CompositionMessage =
|
|
522
|
-
| { name: "
|
|
523
|
-
;
|
|
721
|
+
| { name: "myMessage" } & MessageSource;
|
|
524
722
|
|
|
525
723
|
// Combine the local messages and the ones from LoadSettings
|
|
526
724
|
export type Message =
|
|
527
725
|
| CompositionMessage
|
|
528
|
-
| LoadSettings.Message
|
|
529
|
-
;
|
|
726
|
+
| LoadSettings.Message;
|
|
530
727
|
|
|
531
728
|
const MsgSource: MessageSource = { source: "Composition" };
|
|
532
729
|
|
|
533
730
|
export const Msg = {
|
|
534
|
-
myMessage: (): Message => ({ name: "
|
|
731
|
+
myMessage: (): Message => ({ name: "myMessage", ...MsgSource }),
|
|
732
|
+
...LoadSettings.Msg,
|
|
535
733
|
};
|
|
536
734
|
|
|
537
|
-
const cmd =
|
|
735
|
+
const cmd = createCmd<Message>();
|
|
538
736
|
|
|
539
737
|
// Include the LoadSettings Model
|
|
540
|
-
export
|
|
738
|
+
export interface Model extends LoadSettings.Model {
|
|
541
739
|
// ...
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
export const init = (): [Model, Elm.Cmd<Message>] => {
|
|
545
|
-
const model: Model = {
|
|
546
|
-
// Spread the initial model from LoadSettings
|
|
547
|
-
...LoadSettings.init(),
|
|
548
|
-
// ...
|
|
549
|
-
};
|
|
740
|
+
}
|
|
550
741
|
|
|
742
|
+
export const init = (): [Model, Cmd<Message>] => {
|
|
551
743
|
// Return the model and dispatch the LoadSettings message
|
|
552
|
-
return [
|
|
744
|
+
return [
|
|
745
|
+
{
|
|
746
|
+
// Spread the initial model from LoadSettings
|
|
747
|
+
...LoadSettings.init(),
|
|
748
|
+
// ...
|
|
749
|
+
},
|
|
750
|
+
cmd.ofMsg(Msg.loadSettings())
|
|
751
|
+
];
|
|
553
752
|
};
|
|
554
753
|
|
|
555
754
|
// In our update function, we first distinguish between the sources of the messages
|
|
556
|
-
export
|
|
755
|
+
export function update (model: Model, msg: Message): UpdateReturnType<Model, Message> {
|
|
557
756
|
switch (msg.source) {
|
|
558
757
|
case "Composition":
|
|
559
758
|
// Then call the update function for the local messages
|
|
@@ -568,7 +767,7 @@ export const update = (model: Model, msg: Message): Elm.UpdateReturnType<Model,
|
|
|
568
767
|
// For the msg parameter we use the local CompositionMessage type
|
|
569
768
|
const updateComposition = (model: Model, msg: CompositionMessage): Elm.UpdateReturnType<Model, Message> => {
|
|
570
769
|
switch (msg.name) {
|
|
571
|
-
case "
|
|
770
|
+
case "myMessage":
|
|
572
771
|
return [{}];
|
|
573
772
|
}
|
|
574
773
|
}
|
|
@@ -585,12 +784,12 @@ To inform the parent component about some action, let's say to close a dialog fo
|
|
|
585
784
|
```ts Dialog.ts
|
|
586
785
|
export type Message =
|
|
587
786
|
...
|
|
588
|
-
| { name: "
|
|
787
|
+
| { name: "close" }
|
|
589
788
|
...
|
|
590
789
|
|
|
591
790
|
export const Msg = {
|
|
592
791
|
...
|
|
593
|
-
close: (): Message => ({ name: "
|
|
792
|
+
close: (): Message => ({ name: "close" }),
|
|
594
793
|
...
|
|
595
794
|
}
|
|
596
795
|
```
|
|
@@ -607,8 +806,9 @@ To inform the parent component about some action, let's say to close a dialog fo
|
|
|
607
806
|
|
|
608
807
|
```ts Dialog.ts
|
|
609
808
|
...
|
|
610
|
-
case "
|
|
809
|
+
case "close":
|
|
611
810
|
props.onClose();
|
|
811
|
+
|
|
612
812
|
return [{}];
|
|
613
813
|
...
|
|
614
814
|
```
|
|
@@ -629,6 +829,7 @@ To test your **update** function you can use some helper functions in `react-elm
|
|
|
629
829
|
| --- | --- |
|
|
630
830
|
| `getOfMsgParams` | Extracts the messages out of a command |
|
|
631
831
|
| `execCmd` | Executes the provided command and returns an array of all messages. |
|
|
832
|
+
| `getUpdateFn` | returns an `update` function for your update map object. |
|
|
632
833
|
|
|
633
834
|
### Testing the model and simple message commands
|
|
634
835
|
|
|
@@ -690,10 +891,10 @@ it("returns the correct cmd", () => {
|
|
|
690
891
|
|
|
691
892
|
### Testing with an UpdateMap
|
|
692
893
|
|
|
693
|
-
To test your update map
|
|
894
|
+
To test your update map, you can get an `update` function by calling `getUpdateFn`:
|
|
694
895
|
|
|
695
896
|
```ts
|
|
696
|
-
import
|
|
897
|
+
import { getUpdateFn } from "react-elmish/dist/Testing";
|
|
697
898
|
|
|
698
899
|
const update = getUpdateFn(updateMap);
|
|
699
900
|
|
|
@@ -701,7 +902,7 @@ const update = getUpdateFn(updateMap);
|
|
|
701
902
|
const [model, cmd] = update(msg, model, props);
|
|
702
903
|
```
|
|
703
904
|
|
|
704
|
-
## Migration from v1.x to
|
|
905
|
+
## Migration from v1.x to v2.x
|
|
705
906
|
|
|
706
907
|
* Use `Logger` and `Message` instead of `ILogger` and `IMessage`.
|
|
707
908
|
* The global declaration of the `Nullable` type was removed, because it is unexpected for this library to declare such a type. You can declare this type for yourself if needed:
|
|
@@ -711,3 +912,16 @@ const [model, cmd] = update(msg, model, props);
|
|
|
711
912
|
type Nullable<T> = T | null;
|
|
712
913
|
}
|
|
713
914
|
```
|
|
915
|
+
|
|
916
|
+
## Migration from v2.x to v3.x
|
|
917
|
+
|
|
918
|
+
The signature of `useElmish` has changed. It takes an options object now. Thus there is no need for the `useElmishMap` function. Use the new `useElmish` hook with an `UpdateMap` instead.
|
|
919
|
+
|
|
920
|
+
To use the old `useElmish` and `useElmishMap` functions, import them from the legacy namespace:
|
|
921
|
+
|
|
922
|
+
```ts
|
|
923
|
+
import { useElmish } from "react-elmish/dist/legacy/useElmish";
|
|
924
|
+
import { useElmishMap } from "react-elmish/dist/legacy/useElmishMap";
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
**Notice**: These functions are marked as deprecated and will be removed in a later release.
|