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 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 * as Elm from "react-elmish";
28
+ import { Cmd, createCmd, InitResult, UpdateReturnType, UpdateMap } from "react-elmish";
29
29
 
30
30
  export type Message =
31
- | { name: "Increment" }
32
- | { name: "Decrement" }
33
- ;
31
+ | { name: "increment" }
32
+ | { name: "decrement" };
34
33
  ```
35
34
 
36
- You can also create some convenience functions to dispatch a message:
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: "Increment" }),
41
- decrement: (): Message => ({ name: "Decrement" }),
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 type Model = Readonly<{
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 type Props = {
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 const init = (props: Props): [Model, Elm.Cmd<Message>] => {
71
- const model: Model = {
72
- value: props.initialValue,
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
- return [model, cmd.none];
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
- To update the model based on a message we need an **update** function:
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): Elm.UpdateReturnType<Model, Message> => {
91
+ export const update = (model: Model, msg: Msg, props: Props): UpdateReturnType<Model, Message> => {
83
92
  switch (msg.name) {
84
- case "Increment":
93
+ case "increment":
85
94
  return [{ value: model.value + 1 }];
86
95
 
87
- case "Decrement":
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
- This can be a **class component**:
108
+ As a **function component**:
100
109
 
101
110
  ```tsx
102
111
  // Import everything from the App.ts
103
- import * as Shared from "../App";
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<Shared.Model, Shared.Message, Shared.Props> {
143
+ class App extends ElmComponent<Model, Message, Props> {
111
144
  // Construct the component with the props and init function
112
- constructor(props: Shared.Props) {
113
- super(props, Shared.init, "App");
145
+ constructor(props: Props) {
146
+ super(props, init, "App");
114
147
  }
115
148
 
116
149
  // Assign our update function to the component
117
- update = Shared.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(Shared.Msg.increment())}>Increment</button>
130
- <button onClick={() => this.dispatch(Shared.Msg.decrement())}>Decrement</button>
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: "Increment", step?: number }
183
+ | { name: "increment", step?: number }
204
184
  ...
205
185
 
206
186
  export const Msg = {
207
- increment: (step?: number): Message => ({ name: "Increment", step }),
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 "Increment":
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
- ## Setup
247
+ ## Dispatch commands in the update map or update function
268
248
 
269
- **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.
249
+ In addition to modifying the model, you can dispatch new commands here.
270
250
 
271
- ```ts
272
- import * as Elm from "react-elmish";
251
+ To do so, you have to create a `cmd` object:
273
252
 
274
- const myLogger = {
275
- debug(...args: unknown []) {
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
- Elm.init({
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
- The error middleware function is called by the `handleError` function (see [Error handling](#error-handling)).
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: "PrintLastMessage", message: string }
281
+ | { name: "printLastMessage", message: string }
356
282
  ...
357
283
 
358
284
  export const Msg = {
359
285
  ...
360
- printLastMessage: (message: string): Message => ({ name: "PrintLastMessage", message }),
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 "Increment":
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** function.
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: "LoadSettings" },
391
- | { name: "SettingsLoaded", settings: Settings }
392
- | { name: "Error", error: Error }
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: "LoadSettings" }),
398
- settingsLoaded: (settings: Settings): Message => ({ name: "SettingsLoaded", settings }),
399
- error: (error: Error): Message => ({ name: "Error", error }),
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 "LoadSettings":
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 "SettingsLoaded":
343
+ case "settingsLoaded":
416
344
  return [{ settings: msg.settings }];
417
345
 
418
- case "Error":
419
- return Elm.handleError(msg.error);
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 * as Elm from "react-elmish";
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 = Elm.MsgSource<"LoadSettings">;
660
+ type MessageSource = MsgSource<"LoadSettings">;
462
661
 
463
662
  // Add that MessageSource to all the messages
464
663
  export type Message =
465
- | { name: "LoadSettings" } & MessageSource
466
- | { name: "SettingsLoaded", settings: Settings } & MessageSource
467
- | { name: "Error", error: Error } & MessageSource
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: "LoadSettings", ...MsgSource }),
474
- settingsLoaded: (settings: Settings): Message => ({ name: "SettingsLoaded", settings, ...MsgSource }),
475
- error: (error: Error): Message => ({ name: "Error", error, ...MsgSource }),
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 = Elm.createCmd<Message>();
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): Elm.UpdateReturnType<Model, Message> => {
687
+ export const update = (_model: Model, msg: Message): UpdateReturnType<Model, Message> => {
489
688
  switch (msg.name) {
490
- case "LoadSettings":
689
+ case "loadSettings":
491
690
  return [{}, cmd.ofPromise.either(loadSettings, Msg.settingsLoaded, Msg.error)];
492
691
 
493
- case "SettingsLoaded":
692
+ case "settingsLoaded":
494
693
  return [{ settings: msg.settings }];
495
694
 
496
- case "Error":
497
- return Elm.handleError(msg.error);
695
+ case "error":
696
+ return handleError(msg.error);
498
697
  }
499
698
  };
500
699
 
501
- const loadSettings = async (): Promise<Settings> => {
700
+ async function loadSettings (): Promise<Settings> {
502
701
  // Call some service (e.g. database or backend)
503
- return Promise.resolve({});
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 * as Elm from "react-elmish";
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 = Elm.MsgSource<"Composition">;
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: "MyMessage" } & MessageSource
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: "MyMessage", ...MsgSource }),
731
+ myMessage: (): Message => ({ name: "myMessage", ...MsgSource }),
732
+ ...LoadSettings.Msg,
535
733
  };
536
734
 
537
- const cmd = Elm.createCmd<Message>();
735
+ const cmd = createCmd<Message>();
538
736
 
539
737
  // Include the LoadSettings Model
540
- export type Model = Readonly<{
738
+ export interface Model extends LoadSettings.Model {
541
739
  // ...
542
- }> & LoadSettings.Model;
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 [model, cmd.ofMsg(LoadSettings.Msg.loadSettings())];
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 const update = (model: Model, msg: Message): Elm.UpdateReturnType<Model, Message> => {
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 "MyMessage":
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: "Close" }
787
+ | { name: "close" }
589
788
  ...
590
789
 
591
790
  export const Msg = {
592
791
  ...
593
- close: (): Message => ({ name: "Close" }),
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 "Close":
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 (when using `useElmishMap`), you can get your `update` function by calling `getUpdateFn`:
894
+ To test your update map, you can get an `update` function by calling `getUpdateFn`:
694
895
 
695
896
  ```ts
696
- import * as Testing from "react-elmish/dist/Testing";
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 2.x
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.