After reading the official xstate tutorial, I tried to implement my own machine inspired by this post on dev.to by one of the xstate's dev.
Everything works as expected besides that output
does not seem to be updated. The assignment does not do its job I think. What did I forget?
To compare, here is a working demo from xstate where the variable in the context is updated as expected.
more information on assign on Context | XState Docs
my code:
import "./styles.css";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createMachine, assign } from "xstate";
import { useMachine } from "@xstate/react";
interface FetchContext {
output: string;
}
const fetchifetch = async () => {
return await fetch(
"https://jsonplaceholder.typicode.com/todos/1"
).then((response) => response.json());
};
const fetchMachine = createMachine<FetchContext>({
initial: "idle",
context: {
output: "wesh" // none selected
},
states: {
idle: {
on: {
FETCH: "loading"
}
},
loading: {
invoke: {
src: (context, event) => async (send) => {
setTimeout(async () => {
const data = await fetchifetch();
console.log("done");
console.log(data);
// well. here I want to assign something to output
assign({
output: (context, event) => data.title
});
send("FETCHED_SUCCESSFULLY");
}, 4000);
console.log("start");
},
onError: {
target: "idle"
}
},
on: {
FETCHED_SUCCESSFULLY: {
target: "idle"
}
}
},
fetch: {
on: {
CLOSE: "idle"
}
}
}
});
function App() {
const [current, send] = useMachine(fetchMachine);
const { output } = current.context;
return (
<div className="App">
<h1>XState React Template</h1>
<br />
<input
disabled={current.matches("loading")}
defaultValue="yo"
onChange={(e) => console.log(e.currentTarget.value)}
/>
<button
disabled={current.matches("loading")}
onClick={() => send("FETCH")}
>
click to fetch
</button>
<!-- let's display the result over here -->
<div>{output}</div>
<div>{current.context.output}</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You need to return a Promise then and have the state machine update the context after the Promise is resolved.
The context is updated in the onDone
property of invoke
.
const fetchMachine = createMachine<FetchContext>({
initial: "idle",
context: {
output: "wesh" // none selected
},
states: {
idle: {
on: {
FETCH: "loading"
}
},
loading: {
invoke: {
src: (context, event) => async (send) => {
return new Promise((resolve, reject) => {
setTimeout(async () => {
try {
const data = await fetchifetch();
resolve(data.title);
} catch (err) {
reject(err);
}
}, 4000);
});
},
onDone: {
target: "fetch",
actions: assign({
output: (_, event) => event.data,
})
},
onError: {
target: "idle"
}
},
on: {
FETCHED_SUCCESSFULLY: {
target: "idle",
}
}
},
fetch: {
on: {
CLOSE: "idle"
}
}
}
});