-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement predictableActionArguments
feature flag
#3289
Implement predictableActionArguments
feature flag
#3289
Conversation
🦋 Changeset detectedLatest commit: bb0e0fb The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
👇 Click on the image for a new way to code review
Legend |
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit bb0e0fb:
|
// this is currently required to execute initial actions as the `initialState` gets cached | ||
// we can't just recompute it (and execute actions while doing so) because we try to preserve identity of actors created within initial assigns | ||
_event === initEvent) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very unfortunate because it means that initial actions will actually be called with a completely resolved context. Perhaps I can use a similar logic that is implemented here to overcome this problem:
xstate/packages/core/src/actions.ts
Lines 769 to 775 in 430362d
const contextIndex = preservedContexts.length - 1; | |
resolvedActionObject = { | |
...resolvedActionObject, | |
exec: (_ctx, ...args) => { | |
exec(preservedContexts[contextIndex], ...args); | |
} | |
}; |
But I wonder how we could avoid this sort of thing in v5
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually - right now this just works:
const actual: number[] = [];
const m = createMachine({
predictableActionArguments: true,
context: { count: 0 },
entry: [
(ctx) => actual.push(ctx.count),
assign({ count: 1 }),
(ctx) => actual.push(ctx.count),
assign({ count: 2 }),
(ctx) => actual.push(ctx.count),
],
});
const s = interpret(m);
expect(s.initialState.context).toEqual({ count: 2 });
expect(actual).toEqual([]);
s.start();
expect(actual).toEqual([0, 1, 2]);
But it relies on this exact logic that I've linked and I think that ideally, we should get rid of this exec
mutation in v5. It's a problem for future us though.
actionObject as StopAction<TContext, TEvent>, | ||
updatedContext, | ||
_event | ||
); | ||
predictableExec?.(resolved, updatedContext, _event); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This leads to mutating this.state.children
where this.state
has not yet been updated with the new state. I need to figure out how I can avoid this problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is OK given the current state of things - .children
are already mutated and somewhat unstable: https://codesandbox.io/s/keen-cookies-vfte0i?file=/src/index.js:798-972
I think though that this should be fixed in v5
Bikeshed: |
I've chosen that because it also enables the |
@davidkpiano any thoughts about the core of this PR? I'd like to know how you feel about it before polishing it further |
My primary concern is the spreading of |
"The first" method that receives this argument is already xstate/packages/core/src/StateNode.ts Line 1216 in 430362d
it just calls to some other functions (as the internal machinery is reused, in the same way, it was reused before), and those need to pass it around as the xstate/packages/core/src/StateNode.ts Line 908 in 430362d
This is mostly internal and we might figure out a better algorithm in the future so I wouldn't be bothered about this that much. |
import { raise, stop } from '../src/actions'; | ||
|
||
describe('predictableExec', () => { | ||
it('should call mixed custom and builtin actions in the definitions order', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this does the same thing as preserveActionOrder
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It includes that behavior as per the test here but this test is about something different.
At the moment if custom actions are executed within the interpreter so when the machine.transition
returns back to it. And builtin actions are resolved within the machine.transition
call, so with this:
entry: [
function first() {},
assign(function second() {})
]
we might actually observe those functions to be called in the unpredictable order. If we'd put console.logs in them we would see:
second
first
With this PR the order of execution matches more common expectations, resolving builtin actions is immediately followed by executing them, like here:
xstate/packages/core/src/actions.ts
Lines 667 to 672 in 430362d
const resolved = resolveLog( | |
actionObject as LogAction<TContext, TEvent>, | |
updatedContext, | |
_event | |
); | |
predictableExec?.(resolved, updatedContext, _event); |
And thus we would observe:
first
second
Co-authored-by: David Khourshid <[email protected]>
…eding assign actions
@@ -584,4 +585,25 @@ describe('predictableExec', () => { | |||
|
|||
expect(actual).toEqual(['stop 1', 'start 2']); | |||
}); | |||
|
|||
// TODO: this might be tricky in v5 because this currently relies on `.exec` property being mutated to capture the context values in a closure |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this will be tricky, especially if we execute actions eagerly in v5, as you suggested?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The question here is - what exactly the machine.initialState
is and what context value does it hold? Does it already have assigns from a top-level entry resolved? remember that machine.initialState.context
has to be pure so we cant yet execute actions eagerly at this point in time
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's why currently there's the notion of preInitialState
in v5, which represents the initial state and actions without any assignments performed nor actions executed.
In my view, this is what machine.initialState
(the pure getter) should be - state without actions executed. Then when the machine starts (is interpreted), the actions should be executed.
So in this test, machine.initialState
would have context { count: 0 }
since no actions were executed yet, but the read state from interpreter.subscribe(state => ...)
would have { count: 2 }
since the state after the initial state has those actions executed.
This would also be what the input RFC would solve, since there would be no need for initial assign
actions.
Half-baked thoughts, just thinking out loud
What remains left for this to be merged in? |
@davidkpiano still working on adding support for this in the typegen, I've just pushed out an "enabler" PR for this work today: statelyai/xstate-tools#170 . I still need to build on top of that but this refactor allows me to extend the current shape of the code in an easier way |
I've reverted the change related to making a NULL event a pseudo-event with this flag. Because of that, all the entry actions following always transitions would have to be callable with |
Co-authored-by: David Khourshid <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this looks great!
@@ -1,5 +1,25 @@ | |||
# Actions | |||
|
|||
::: warning | |||
|
|||
It is advised to configure `predictableActionArguments: true` at the top-level of your machine config, like this: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add the XState version this applies to?
@@ -662,6 +662,10 @@ export interface StateNodeConfig< | |||
* @default false | |||
*/ | |||
preserveActionOrder?: boolean; | |||
/** | |||
* @default false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @default false | |
* Whether XState calls actions with the event directly responsible for the related transition. | |
* @default false |
- XState will always call an action with the event directly responsible for the related transition, | ||
- you also automatically opt-into [`preserveActionOrder`](https://xstate.js.org/docs/guides/context.html#action-order). | ||
|
||
Please be aware that you might not able to use `state` from the `meta` argument when using this flag. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can y'all add more information how to migrate? We had trouble finding a replacement for using this. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you open a discussion that would describe your use case for using this property?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although this PR's main goal wasn't that - it can be used to supersede #3210 and thus it fixes the problem described here
The main goal of this work is to:
do not call/resolve actions with an explicit NULL event ({ type: '' }
). Instead, we are calling them with the source event as the always transitions are also called eventless transitions (this matches SCXML spec etc)