Skip to content
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

[core] Add state.can(event) #2546

Merged
merged 14 commits into from
Sep 10, 2021
42 changes: 42 additions & 0 deletions .changeset/nice-pugs-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
'xstate': minor
---

You can now know if an event will cause a state change by using the new `state.can(event)` method, which will return `true` if an interpreted machine will "change" the state when sent the `event`, or `false` otherwise:

```js
const machine = createMachine({
initial: 'inactive',
states: {
inactive: {
on: {
TOGGLE: 'active'
}
},
active: {
on: {
DO_SOMETHING: { actions: ['something'] }
}
}
}
});

const state = machine.initialState;

state.can('TOGGLE'); // true
state.can('DO_SOMETHING'); // false

// Also takes in full event objects:
state.can({
type: 'DO_SOMETHING',
data: 42
}); // false
```

A state is considered "changed" if any of the following are true:

- its `state.value` changes
- there are new `state.actions` to be executed
- its `state.context` changes

See [`state.changed` (documentation)](https://xstate.js.org/docs/guides/states.html#state-changed) for more details.
27 changes: 24 additions & 3 deletions packages/core/src/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import {
StateSchema,
TransitionDefinition,
Typestate,
ActorRef
ActorRef,
StateMachine,
SimpleEventsOf
} from './types';
import { EMPTY_ACTIVITY_MAP } from './constants';
import { matchesState, keys, isString } from './utils';
import { matchesState, keys, isString, warn } from './utils';
import { StateNode } from './StateNode';
import { getMeta, nextEvents } from './stateUtils';
import { initEvent } from './actions';
import { IS_PRODUCTION } from './environment';

export function stateValuesEqual(
a: StateValue | undefined,
Expand Down Expand Up @@ -128,6 +131,7 @@ export class State<
*/
public children: Record<string, ActorRef<any>>;
public tags: Set<string>;
public machine: StateMachine<TContext, any, TEvent, TTypestate> | undefined;
davidkpiano marked this conversation as resolved.
Show resolved Hide resolved
/**
* Creates a new State instance for the given `stateValue` and `context`.
* @param stateValue
Expand Down Expand Up @@ -251,6 +255,7 @@ export class State<
this.tags =
(Array.isArray(config.tags) ? new Set(config.tags) : config.tags) ??
new Set();
this.machine = config.machine;

Object.defineProperty(this, 'nextEvents', {
get: () => {
Expand Down Expand Up @@ -283,7 +288,7 @@ export class State<
}

public toJSON() {
const { configuration, transitions, tags, ...jsonValues } = this;
const { configuration, transitions, tags, machine, ...jsonValues } = this;

return { ...jsonValues, tags: Array.from(tags) };
}
Expand Down Expand Up @@ -314,4 +319,20 @@ export class State<
public hasTag(tag: string): boolean {
return this.tags.has(tag);
}

/**
* Determines whether sending the `event` will cause a transition.
* @param event The event to test
* @returns Whether the event will cause a transition
*/
public can(event: TEvent | SimpleEventsOf<TEvent>['type']): boolean {
if (IS_PRODUCTION) {
warn(
!!this.machine,
`state.can(...) used outside of a machine-created State object; this will always return false.`
);
}
davidkpiano marked this conversation as resolved.
Show resolved Hide resolved

return !!this.machine?.transition(this, event).changed;
}
}
3 changes: 2 additions & 1 deletion packages/core/src/StateNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,8 @@ class StateNode<
transitions: stateTransition.transitions,
children,
done: isDone,
tags: currentState?.tags
tags: currentState?.tags,
machine: this
});

const didUpdateContext = currentContext !== updatedContext;
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,26 @@ export type Action<TContext, TEvent extends EventObject> =
/**
* Extracts action objects that have no extra properties.
*/
type SimpleActionsFrom<T extends BaseActionObject> = ActionObject<
type SimpleActionsOf<T extends BaseActionObject> = ActionObject<
any,
any
> extends T
? T // If actions are unspecified, all action types are allowed (unsafe)
: ExtractWithSimpleSupport<T>;

/**
* Events that do not require payload
*/
export type SimpleEventsOf<
TEvent extends EventObject
> = ExtractWithSimpleSupport<TEvent>;

export type BaseAction<
TContext,
TEvent extends EventObject,
TAction extends BaseActionObject
> =
| SimpleActionsFrom<TAction>['type']
| SimpleActionsOf<TAction>['type']
| TAction
| RaiseAction<any>
| SendAction<TContext, TEvent, any>
Expand Down Expand Up @@ -1247,6 +1254,7 @@ export interface StateConfig<TContext, TEvent extends EventObject> {
children: Record<string, ActorRef<any>>;
done?: boolean;
tags?: Set<string>;
machine?: StateMachine<TContext, any, TEvent, any>;
}

export interface StateSchema<TC = any> {
Expand Down
Loading