aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKero van Gelder <keroami@users.noreply.github.com>2023-04-05 14:33:00 +0200
committerGitHub <noreply@github.com>2023-04-05 13:33:00 +0100
commita1b2f0344a59e24ae4d16f1689a02ba72ced2b53 (patch)
tree83843c6afd048b2c770a811696e1947bf915ffaa
parent2feb2a8e1a427cb7545785e2678e4523533c432e (diff)
downloadlustre-a1b2f0344a59e24ae4d16f1689a02ba72ced2b53.tar.gz
lustre-a1b2f0344a59e24ae4d16f1689a02ba72ced2b53.zip
🐛 Avoid a race condition when dispatch is used twice from outside (#8)
* Upgrade examples to lustre 2.0 * Upgrade to gleam 0.27.0 * Fix: avoid a race condition when dispatch is used twice from outside E.g. websocket open + websocket msg arriving Application returns a Cmd from update(model, msg) but the Effect handler is *not* ran be React 18 before the websocket msg-arriving is handled by the Reducer. Result: The Cmd returned by update(model, msg) is dropped silently. --------- Co-authored-by: Kero van Gelder <kero@chmeee.org>
-rw-r--r--manifest.toml2
-rw-r--r--src/ffi.mjs29
-rw-r--r--test/example/application/counter.gleam10
-rw-r--r--test/example/main.gleam2
-rw-r--r--test/playground/monaco.gleam2
5 files changed, 31 insertions, 14 deletions
diff --git a/manifest.toml b/manifest.toml
index 84bfb11..ee5fc14 100644
--- a/manifest.toml
+++ b/manifest.toml
@@ -2,7 +2,7 @@
# You typically do not need to edit this file
packages = [
- { name = "gleam_stdlib", version = "0.26.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "B17BBE8A78F3909D93BCC6C24F531673A7E328A61F24222EB1E58D0A7552B1FE" },
+ { name = "gleam_stdlib", version = "0.27.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "9DBDD21B48C654182CDD8AA15ACF85E8E74A0438583C68BD7EF08BE89F999C6F" },
{ name = "gleeunit", version = "0.10.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "ECEA2DE4BE6528D36AFE74F42A21CDF99966EC36D7F25DEB34D47DD0F7977BAF" },
]
diff --git a/src/ffi.mjs b/src/ffi.mjs
index cb458d4..b6883d4 100644
--- a/src/ffi.mjs
+++ b/src/ffi.mjs
@@ -35,20 +35,37 @@ export const mount = ({ init, update, render }, selector) => {
//
let dispatch = null;
+ let init_cmds = null;
+
const App = React.createElement(() => {
- const [[state, cmds], $dispatch] = React.useReducer(
- ([state, _], action) => update(state, action),
- init
+ const [state, $dispatch] = React.useReducer(
+ (state, action) => {
+ let [new_state, cmds] = update(state, action)
+ // Handle cmds immediately, they're not React-kind-of-state
+ for (const cmd of Cmd.to_list(cmds)) {
+ cmd(dispatch);
+ }
+ return new_state
+ },
+ undefined,
+ () => {
+ let [state, cmds] = init
+ // postpone handling cmds, as we do not have the dispatch, yet
+ init_cmds = cmds
+ return state
+ }
);
- const el = render(state);
if (dispatch === null) dispatch = $dispatch;
+ const el = render(state);
+
React.useEffect(() => {
- for (const cmd of Cmd.to_list(cmds)) {
+ for (const cmd of Cmd.to_list(init_cmds)) {
cmd($dispatch);
}
- }, [cmds]);
+ init_cmds = undefined; // Just so we get an error, rather than re-execute
+ }, []); // empty deps array means this effect will only run on initial render
return typeof el == "string" ? el : el($dispatch);
});
diff --git a/test/example/application/counter.gleam b/test/example/application/counter.gleam
index 40c93e3..62d8599 100644
--- a/test/example/application/counter.gleam
+++ b/test/example/application/counter.gleam
@@ -5,7 +5,7 @@ import lustre
import lustre/attribute.{ style }
import lustre/cmd.{ Cmd }
import lustre/element.{ button, div, p, text }
-import lustre/event.{ dispatch, on_click }
+import lustre/event.{ on_click }
// MAIN ------------------------------------------------------------------------
@@ -16,7 +16,7 @@ pub fn main () -> Nil {
// `lustre.start` can return an `Error` if no DOM element is found that matches
// the selector. This is a fatal error for our examples, so we panic if that
// happens.
- assert Ok(_) = lustre.start(program, selector)
+ let assert Ok(_) = lustre.start(program, selector)
Nil
}
@@ -77,10 +77,10 @@ fn tick () -> Cmd(Action) {
// RENDER ----------------------------------------------------------------------
-pub fn render (state) {
+pub fn render (state: State) {
div([ style([ #("display", "flex") ]) ], [
- button([ on_click(dispatch(Decr)) ], [ text("-") ]),
+ button([ on_click(Decr) ], [ text("-") ]),
p([], [ int.to_string(state.count) |> text ]),
- button([ on_click(dispatch(Incr)) ], [ text("+") ])
+ button([ on_click(Incr) ], [ text("+") ]),
])
}
diff --git a/test/example/main.gleam b/test/example/main.gleam
index 7d9c063..21c4067 100644
--- a/test/example/main.gleam
+++ b/test/example/main.gleam
@@ -15,7 +15,7 @@ pub fn main () -> Nil {
// `lustre.start` can return an `Error` if no DOM element is found that matches
// the selector. This is a fatal error for our examples, so we panic if that
// happens.
- assert Ok(dispatch) = lustre.start(program, selector)
+ let assert Ok(dispatch) = lustre.start(program, selector)
dispatch(Counter(counter.Incr))
dispatch(Counter(counter.Incr))
diff --git a/test/playground/monaco.gleam b/test/playground/monaco.gleam
index f0927ca..14958ac 100644
--- a/test/playground/monaco.gleam
+++ b/test/playground/monaco.gleam
@@ -8,7 +8,7 @@ pub external fn render (attributes: List(Attribute(action))) -> Element(action)
pub fn on_change (handler: fn (String, fn (action) -> Nil) -> Nil) -> Attribute(action) {
event.on("change", fn (e, dispatch) {
- assert Ok(code) = dynamic.string(e)
+ let assert Ok(code) = dynamic.string(e)
handler(code, dispatch)
})