Closed SandroMaglione closed 2 months ago
Latest commit: 08932cdecdd31553729a34799de6c1afbff9ef4d
The changes in this PR will be included in the next version bump.
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
It's probably not possible to make the same update to useActorRef for react and vue since the type is made optional by spreading options as the last function argument, but useActorRef has a third parameter observerOrListener, which does not allow to spread options to make them optional:
You can create [options?, observerOrListener?]
, make the first element required conditionally and spread the result. This way you will make the second argument conditionally required while keeping the third one optional
You can create
[options?, observerOrListener?]
, make the first element required conditionally and spread the result. This way you will make the second argument conditionally required while keeping the third one optional
@Andarist not sure this works. I tried various implementations, but the issue is that making a parameter conditionally required requires to spread it, but spread is allowed only for the last parameter.
This below doesn’t work A rest element must be last in a destructuring pattern.ts(2462)
:
export function useActorRef<TLogic extends AnyActorLogic>(
machine: TLogic,
...[...[options], observerOrListener]: [
options: ConditionalRequired<
[
options?: ActorOptions<TLogic> & {
[K in RequiredOptions<TLogic>]: unknown;
}
],
IsNotNever<RequiredOptions<TLogic>>
>,
observerOrListener?:
| Observer<SnapshotFrom<TLogic>>
| ((value: SnapshotFrom<TLogic>) => void)
]
): Actor<TLogic>
These two solutions below make the parameter always required:
export function useActorRef<TLogic extends AnyActorLogic>(
machine: TLogic,
...[options, observerOrListener]: [
options: ConditionalRequired<
[
options?: ActorOptions<TLogic> & {
[K in RequiredOptions<TLogic>]: unknown;
}
],
IsNotNever<RequiredOptions<TLogic>>
>,
observerOrListener?:
| Observer<SnapshotFrom<TLogic>>
| ((value: SnapshotFrom<TLogic>) => void)
]
): Actor<TLogic>
export function useActorRef<TLogic extends AnyActorLogic>(
machine: TLogic,
...[options, observerOrListener]: [
ConditionalRequired<
[
options?: ActorOptions<TLogic> & {
[K in RequiredOptions<TLogic>]: unknown;
}
],
IsNotNever<RequiredOptions<TLogic>>
>['0'],
observerOrListener?:
| Observer<SnapshotFrom<TLogic>>
| ((value: SnapshotFrom<TLogic>) => void)
]
): Actor<TLogic>
I also tried other options without much success. What do you think?
Those type helpers are not too readable anyway today so I wouldn't try to reuse them at all costs. At the very least something along those lines should work:
export function useActorRef<TLogic extends AnyActorLogic>(
machine: TLogic,
...[options, observerOrListener]: IsNotNever<
RequiredOptions<TLogic>
> extends true
? [
options: ActorOptions<TLogic> & {
[K in RequiredOptions<TLogic>]: unknown;
},
observerOrListener?:
| Observer<SnapshotFrom<TLogic>>
| ((value: SnapshotFrom<TLogic>) => void),
]
: [
options?: ActorOptions<TLogic>,
observerOrListener?:
| Observer<SnapshotFrom<TLogic>>
| ((value: SnapshotFrom<TLogic>) => void),
]
): Actor<TLogic>;
I don't mind some mild repetition.
@Andarist added type tests for required input and changeset. I marked this as a patch release, since I would consider this a bug fix. Let me know if this works.
LGTM - I just left one comment to address
My useMachine calls requires an empty object as a second parameter after this PR, even if there is no input required in any actors. Is that just the price I pay here?
@pckilgore please provide a repro case so we can take a look at your case
@pckilgore if your input is in the form { input: { value?: string; }}
it will result required since only value
is optional, whereas input
is required to be defined
I was able to fix by updating xstate itself to 5.19 from 5.18. Yarn didn't give me any peer dependency warnings about the mismatch, which isn't what I would have expected?
Sorry to bother.
@pckilgore if your input is in the form
{ input: { value?: string; }}
it will result required since onlyvalue
is optional, whereasinput
is required to be defined
That would have still been odd, because I would have expected typescript to accept { input: {} }
in that circumstance, not just {}
as the second parameter. Appreciate the suggestion!
Yes, I would expect { input: {} }
as well. Was it working with just {}
? If so I wouldn’t be sure, definitely a repro is needed
This PR updates the types to require
input
when defined insidetypes/input
when usinguseActor
,useMachine
, anduseActorRef
.Rationale
With the current types even if
types/input
is defined, when using hooks likeuseActor
theinput
option remains optional.In my opinion this is an undesired behaviour, since it should be safe to assume that if the user defines an input it's expected to be provided when using the actor.
More than once I encountered runtime issues caused by forgetting to pass the input.
Solution
This PR copies the implementation of
createActor
that requires to provideinput
when defined.Issue with
useActorRef
It's probably not possible to make the same update to
useActorRef
for react and vue since the type is made optional by spreading options as the last function argument, butuseActorRef
has a third parameterobserverOrListener
, which does not allow to spreadoptions
to make them optional:https://github.com/statelyai/xstate/blob/1ab974547f2e1f1b656279f144f6b88a4419d87e/packages/core/src/createActor.ts#L792-L799
https://github.com/statelyai/xstate/blob/1ab974547f2e1f1b656279f144f6b88a4419d87e/packages/xstate-react/src/useActorRef.ts#L45-L51
I opted instead to make
input
required whileoptions
is still optional. This makesinput
required when passingoptions
, but ifoptions
is not passed the type will work even iftypes/input
is defined:https://github.com/statelyai/xstate/blob/10ed0484e8d3c4f36c125b2c1d7bfdd4770aba20/packages/xstate-react/src/useActorRef.ts#L55-L68
A solution may be to merge
observerOrListener
insideoptions
. This may be a breaking change.