different behavior going from v4 to v5 - interaction with always
#4851
Replies: 2 comments 1 reply
-
Can you please provide a code example? You can make it in v4 or v5; I'll migrate it and compare the difference. |
Beta Was this translation helpful? Give feedback.
-
@davidkpiano You are such a gent. Somehow it comes through and through your org at Stately... is there some sort of pun in that statement? Anyway, I know figuring out other people's code is not easy and a min example is helpful. I appreciate your willing to take a look yourself. As I mentioned in the original post, I spent a good part of an hour last week dealing with my clumsiness with the online setup. Here is the code. The model is straightforward. The expected test results in each version of the interpreter are also included. machine configimport {
Machine as createMachine,
assign,
interpret as createActor,
} from "xstate";
const DEBUG = false;
const BUILDING_STATES = {
REQUEST: "Building request",
ANTIREQUEST: "Building antirequest",
};
export const selectionModelMachine = (config) =>
createMachine(
{
id: "selectionModelMachine",
initial: "idle",
context: {
selectionModel: {},
},
states: {
idle: {
on: {
init: {
target: "Building request",
actions: assign({
selectionModel: (context, event) => ({
...context.selectionModel,
...event.selectionModel,
}),
}),
},
},
},
[BUILDING_STATES.REQUEST]: {
entry: [
assign({
selectionModel: (context) => ({
...context.selectionModel,
requestType: "REQUEST",
requests: { __ALL__: { value: "__ALL__", request: true } },
}),
}),
(context) => {
if (DEBUG) {
console.debug("✨ building request context", context);
}
},
{ type: "dispatchContext" },
],
always: {
cond: "shouldSwitchBuilder",
target: BUILDING_STATES.ANTIREQUEST,
},
on: {
onRowClick: {
actions: [
assign({
selectionModel: updateSelectionModelCfg(
BUILDING_STATES.REQUEST,
DEBUG
),
}),
{ type: "dispatchContext" },
],
},
onToggleAll: {
cond: "isDeselectAll",
target: BUILDING_STATES.ANTIREQUEST,
},
},
},
[BUILDING_STATES.ANTIREQUEST]: {
entry: [
assign({
selectionModel: (context) => ({
...context.selectionModel,
requestType: "ANTIREQUEST",
requests: { __all__: { value: "__ALL__", request: false } },
}),
}),
(context) => {
if (DEBUG) {
console.debug("💥 building antirequest context", context);
}
},
{ type: "dispatchContext" },
],
always: {
cond: "shouldSwitchBuilder",
target: BUILDING_STATES.REQUEST,
},
on: {
onRowClick: {
actions: [
assign({
selectionModel: updateSelectionModelCfg(
BUILDING_STATES.ANTIREQUEST,
DEBUG
),
}),
{ type: "dispatchContext" },
],
},
onToggleAll: {
cond: "isSelectAll",
target: BUILDING_STATES.REQUEST,
},
},
},
},
},
{
actions: {
dispatchContext: (context) => {
console.debug(
"📬 dispatchContext not configured",
context.selectionModel
);
},
},
guards: {
shouldSwitchBuilder: (context) => {
const { selectionModel } = context;
const requestsCount = Object.keys(selectionModel.requests).length - 1; // exclude __all__
const result = requestsCount === selectionModel.totalRowCount;
if (DEBUG) {
console.debug("🔥 shouldSwitchBuilder", result, requestsCount);
}
return result;
},
isSelectAll: (_, event) => event.isSelected,
isDeselectAll: (_, event) => !event.isSelected,
},
}
).withConfig(config);
/**
* Computes the value of the selectionModel depending on the state of the
* machine - either Building request or Building antirequest.
*
* Returns a function that takes ({ context, event }) and returns the updated
* selectionModel.
*
* @function
* @param {string} BUILDING_STATES
* @returns {Function} updateSelectionModel
*/
function updateSelectionModelCfg(state, DEBUG = false) {
// unify the logic based on state
const notOrId = state === BUILDING_STATES.REQUEST ? (x) => x : (x) => !x;
return (context, event) => {
if (DEBUG) {
console.debug("👉 event", event);
}
let newRequests = { ...context.selectionModel.requests };
if (notOrId(event.isSelected)) {
// when true, delete the entry
const { [event.id]: _, ...rest } = newRequests;
newRequests = rest;
} else {
// Otherwise, add or update the entry
newRequests[event.id] = {
value: event.id,
request: notOrId(false),
};
}
return {
...context.selectionModel,
requests: newRequests,
};
};
}
/**
* @function
* @returns {ActorRef} actor
*/
export function init(config, LOG=true) {
const actor = createActor(selectionModelMachine(config));
if (LOG) {
actor.subscribe((state) =>
console.log(JSON.stringify(state.context.selectionModel, null, 2))
);
}
return actor;
}
/**
* @function
* @param {ActorRef} actor
* @returns void
*/
export function test(actor, testHandler = () => {}, DEBUG = false) {
const initEvent = {
selectionModel: {
requests: {},
totalRowCount: 3,
},
};
let results = [];
let test = 0;
// simulate events
testHandler((context, event) => {
test += 1;
results.push({
test: event,
result: Object.keys(context.selectionModel.requests).length,
});
});
if (DEBUG) {
console.log("-------------");
}
actor.send({ type: "onRowClick", id: "row1", isSelected: false }); // 1
actor.send({ type: "onRowClick", id: "row1", isSelected: true });
if (DEBUG) {
console.log("-------------");
}
actor.send({ type: "onToggleAll", isSelected: false }); // 3
actor.send({ type: "onRowClick", id: "row2", isSelected: true });
actor.send({ type: "onRowClick", id: "row2", isSelected: false });
if (DEBUG) {
console.log("------------- change builder: end on __ALL__ true");
}
actor.send({ type: "init", selectionModel: initEvent.selectionModel });
actor.send({ type: "onToggleAll", isSelected: false }); // 6
actor.send({ type: "onRowClick", id: "row1", isSelected: true });
actor.send({ type: "onRowClick", id: "row2", isSelected: true });
actor.send({ type: "onRowClick", id: "row3", isSelected: true }); // 9
if (DEBUG) {
console.log("------------- change builder: end on __ALL__ false");
}
actor.send({ type: "onToggleAll", isSelected: true }); // 10
actor.send({ type: "onRowClick", id: "row1", isSelected: false });
actor.send({ type: "onRowClick", id: "row2", isSelected: false });
actor.send({ type: "onRowClick", id: "row3", isSelected: false }); // 13
console.log("------------- test results -------------");
// v5
// const expected = [2, 1, 1, 2, 1, 2, 3, 4, 1, 2, 3, 4, 1];
// v4
const expected = [2, 1, 1, 2, 1, 2, 3, 1, 1, 2, 3, 1, 1];
results = results.map((result, index) => ({
...result,
pass: result.result === expected[index],
}));
results.forEach((result) => console.log(result));
// print all passed when all pass values are true
console.log(
"\nAll passed?",
results.every((result) => result.pass)
);
} Run this in node. Change up the version to v5 and watch it fail... (the discovery was from the opposite direction when I had to downgrade to v4). node main.jsimport { init, test } from "./selectionModel.js";
let testHandler;
const DEBUG = false;
const dispatchContext = (context, event) => {
if (DEBUG) {
console.debug("🎉", context);
}
if (testHandler) {
testHandler(context, event);
}
return context;
};
function setTestHandler(handler) {
testHandler = handler;
}
export function main() {
const actor = init({ actions: { dispatchContext } }, DEBUG);
actor.start();
actor.send({
type: "init",
selectionModel: { requests: {}, totalRowCount: 3 },
});
test(actor, setTestHandler);
}
main(); The package. package.json with v4{
"name": "Xstate v4 v5 diff",
"version": "0.1.0",
"description": "",
"type": "module",
"scripts": {
"start": "node main.js"
},
"keywords": [],
"dependencies": {
"xstate": "^4.38.3"
}
} |
Beta Was this translation helpful? Give feedback.
-
My apologies for the succinct nature of the post. I did create a detailed post only to loose it while setting up an example on the posted online interpreters.
Anyway, if what I have is useful, I will be happy to expand.
In a nutshell, in a scenario where you have something like
dispatchContext
part of the actions list, while also having aalways: { guard, target }
setup as a "sibling event spec", and where the context changes in the actions list, v4 allows thealways
guard to interrupt the completion of the array of actions. This behavior may or may not be expected. However, what is unexpected, is that the action that was not called, i.e., where the system was interrupted prior to completing the action item, the item gets prepended to the list of the next list of actions.v5 does not seem to let the list of actions get interrupted by the
always
guard evaluating to true.If this is already well documented and understood, then great! Otherwise, I hope this FYI is useful. It's subtle and could be tricky for those that are migrating. I can provide more details if need be.
Beta Was this translation helpful? Give feedback.
All reactions