Skip to content

Commit

Permalink
Rename and repurpose some hooks in @xstate/react (#4006)
Browse files Browse the repository at this point in the history
* useInterpret -> useActorRef

* Replace useActor usage with useSelector

* One more

* useMachine -> useActor

* Remove useSpawn

* Add changeset

* supress some TS errors

* remove unused things

* remove unused types

* remove duplicated things

* remove redundant file

* remove useActor from the context

* move tests around

* fixed types

* add changeset

* Update warnings/errors

* fix tests

---------

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
davidkpiano and Andarist committed May 12, 2023
1 parent d0ba42c commit 42df9a5
Show file tree
Hide file tree
Showing 19 changed files with 1,425 additions and 1,730 deletions.
32 changes: 32 additions & 0 deletions .changeset/gentle-panthers-visit.md
@@ -0,0 +1,32 @@
---
'@xstate/react': major
---

`useActorRef` is introduced, which returns an `ActorRef` from actor logic:

```ts
const actorRef = useActorRef(machine, { ... });
const anotherActorRef = useActorRef(fromPromise(...));
```

~~`useMachine`~~ is deprecated in favor of `useActor`, which works with machines and any other kind of logic

```diff
-const [state, send] = useMachine(machine);
+const [state, send] = useActor(machine);
const [state, send] = useActor(fromTransition(...));
```

~~`useSpawn`~~ is removed in favor of `useActorRef`

````diff
-const actorRef = useSpawn(machine);
+const actorRef = useActorRef(machine);

The previous use of `useActor(actorRef)` is now replaced with just using the `actorRef` directly, and with `useSelector`:

```diff
-const [state, send] = useActor(actorRef);
+const state = useSelector(actorRef, s => s);
// actorRef.send(...)
````
5 changes: 5 additions & 0 deletions .changeset/serious-crews-sort.md
@@ -0,0 +1,5 @@
---
'@xstate/react': patch
---

`useActor` has been removed from the created actor context, you should be able to replace its usage with `MyCtx.useSelector` and `MyCtx.useActorRef`.
16 changes: 15 additions & 1 deletion packages/core/src/types.ts
Expand Up @@ -1801,7 +1801,21 @@ export type SnapshotFrom<T> = ReturnTypeOrValue<T> extends infer R
? TSnapshot
: R extends Interpreter<infer TBehavior>
? SnapshotFrom<TBehavior>
: R extends ActorBehavior<infer _, infer TSnapshot>
: R extends StateMachine<
infer _,
infer __,
infer ___,
infer ____,
infer _____
>
? StateFrom<R>
: R extends ActorBehavior<
infer _,
infer TSnapshot,
infer __,
infer ___,
infer ____
>
? TSnapshot
: R extends ActorContext<infer _, infer TSnapshot, infer __>
? TSnapshot
Expand Down
14 changes: 3 additions & 11 deletions packages/xstate-react/src/createActorContext.ts
@@ -1,6 +1,5 @@
import * as React from 'react';
import { useInterpret } from './useInterpret';
import { useActor as useActorUnbound } from './useActor';
import { useActorRef } from './useActorRef';
import { useSelector as useSelectorUnbound } from './useSelector';
import {
ActorRefFrom,
Expand Down Expand Up @@ -40,7 +39,6 @@ export function createActorContext<TMachine extends AnyStateMachine>(
| Observer<StateFrom<TMachine>>
| ((value: StateFrom<TMachine>) => void)
): {
useActor: () => [StateFrom<TMachine>, ActorRefFrom<TMachine>['send']];
useSelector: <T>(
selector: (snapshot: SnapshotFrom<TMachine>) => T,
compare?: (a: T, b: T) => boolean
Expand Down Expand Up @@ -71,7 +69,7 @@ export function createActorContext<TMachine extends AnyStateMachine>(
children: React.ReactNode;
machine: TMachine;
}) {
const actor = useInterpret(
const actor = (useActorRef as any)(
providedMachine,
interpreterOptions,
observerOrListener
Expand All @@ -82,7 +80,7 @@ export function createActorContext<TMachine extends AnyStateMachine>(

Provider.displayName = `ActorProvider(${machine.id})`;

function useContext() {
function useContext(): ActorRefFrom<TMachine> {
const actor = React.useContext(ReactContext);

if (!actor) {
Expand All @@ -94,11 +92,6 @@ export function createActorContext<TMachine extends AnyStateMachine>(
return actor;
}

function useActor() {
const actor = useContext();
return useActorUnbound(actor);
}

function useSelector<T>(
selector: (snapshot: SnapshotFrom<TMachine>) => T,
compare?: (a: T, b: T) => boolean
Expand All @@ -110,7 +103,6 @@ export function createActorContext<TMachine extends AnyStateMachine>(
return {
Provider: Provider as any,
useActorRef: useContext,
useActor,
useSelector
};
}
7 changes: 4 additions & 3 deletions packages/xstate-react/src/index.ts
@@ -1,7 +1,8 @@
export { useMachine } from './useMachine.ts';
export { useActor } from './useActor.ts';
export { useInterpret } from './useInterpret.ts';
export { useActorRef } from './useActorRef.ts';
export { useSelector } from './useSelector.ts';
export { useSpawn } from './useSpawn.ts';
export { shallowEqual } from './shallowEqual.ts';
export { createActorContext } from './createActorContext.ts';

// deprecated
export { useMachine } from './useMachine.ts';
19 changes: 0 additions & 19 deletions packages/xstate-react/src/types.ts

This file was deleted.

84 changes: 58 additions & 26 deletions packages/xstate-react/src/useActor.ts
@@ -1,16 +1,46 @@
import { useCallback } from 'react';
import { ActorRef, EventObject, SnapshotFrom } from 'xstate';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

export function useActor<TActor extends ActorRef<any, any>>(
actorRef: TActor
): [SnapshotFrom<TActor>, TActor['send']];
export function useActor<TEvent extends EventObject, TSnapshot>(
actorRef: ActorRef<TEvent, TSnapshot>
): [TSnapshot, (event: TEvent) => void];
export function useActor(
actorRef: ActorRef<EventObject, unknown>
): [unknown, (event: EventObject) => void] {
import { useCallback, useEffect } from 'react';
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';
import {
ActorRefFrom,
AnyActorBehavior,
AnyState,
InterpreterOptions,
InterpreterStatus,
SnapshotFrom
} from 'xstate';
import { useIdleInterpreter } from './useActorRef.ts';
import { isActorRef } from 'xstate/actors';

function identity<T>(a: T): T {
return a;
}

const isEqual = (prevState: AnyState, nextState: AnyState) => {
return prevState === nextState || nextState.changed === false;
};

export function useActor<TBehavior extends AnyActorBehavior>(
behavior: TBehavior,
options: InterpreterOptions<TBehavior> = {}
): [
SnapshotFrom<TBehavior>,
ActorRefFrom<TBehavior>['send'],
ActorRefFrom<TBehavior>
] {
if (process.env.NODE_ENV !== 'production') {
if (isActorRef(behavior)) {
throw new Error(
`useActor() expects actor logic (e.g. a machine), but received an ActorRef. Use the useSelector(actorRef, ...) hook instead to read the ActorRef's snapshot.`
);
}
}

const actorRef = useIdleInterpreter(behavior, options as any);

const getSnapshot = useCallback(() => {
return actorRef.getSnapshot();
}, [actorRef]);

const subscribe = useCallback(
(handleStoreChange) => {
const { unsubscribe } = actorRef.subscribe(handleStoreChange);
Expand All @@ -19,21 +49,23 @@ export function useActor(
[actorRef]
);

const boundGetSnapshot = useCallback(
() => actorRef.getSnapshot(),
[actorRef]
);

const storeSnapshot = useSyncExternalStore(
const actorSnapshot = useSyncExternalStoreWithSelector(
subscribe,
boundGetSnapshot,
boundGetSnapshot
getSnapshot,
getSnapshot,
identity,
isEqual
);

const boundSend: typeof actorRef.send = useCallback(
(event) => actorRef.send(event),
[actorRef]
);
useEffect(() => {
actorRef.start();

return () => {
actorRef.stop();
actorRef.status = InterpreterStatus.NotStarted;
(actorRef as any)._initState();
};
}, [actorRef]);

return [storeSnapshot, boundSend];
return [actorSnapshot, actorRef.send, actorRef] as any;
}
110 changes: 110 additions & 0 deletions packages/xstate-react/src/useActorRef.ts
@@ -0,0 +1,110 @@
import { useEffect, useState } from 'react';
import {
AnyActorBehavior,
AnyInterpreter,
AnyStateMachine,
AreAllImplementationsAssumedToBeProvided,
InternalMachineImplementations,
interpret,
ActorRefFrom,
InterpreterOptions,
InterpreterStatus,
Observer,
StateFrom,
toObserver,
SnapshotFrom
} from 'xstate';
import useConstant from './useConstant.ts';
import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect';

export function useIdleInterpreter(
machine: AnyActorBehavior,
options: Partial<InterpreterOptions<AnyActorBehavior>>
): AnyInterpreter {
if (process.env.NODE_ENV !== 'production') {
const [initialMachine] = useState(machine);

if (machine.config !== initialMachine.config) {
console.warn(
`Actor logic has changed between renders. This is not supported and may lead to invalid snapshots.`
);
}
}

const actorRef = useConstant(() => {
return interpret(machine as AnyStateMachine, options);
});

// TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
useIsomorphicLayoutEffect(() => {
(actorRef.behavior as AnyStateMachine).options = (
machine as AnyStateMachine
).options;
});

return actorRef as any;
}

type RestParams<TBehavior extends AnyActorBehavior> =
TBehavior extends AnyStateMachine
? AreAllImplementationsAssumedToBeProvided<
TBehavior['__TResolvedTypesMeta']
> extends false
? [
options: InterpreterOptions<TBehavior> &
InternalMachineImplementations<
TBehavior['__TContext'],
TBehavior['__TEvent'],
TBehavior['__TResolvedTypesMeta'],
true
>,
observerOrListener?:
| Observer<StateFrom<TBehavior>>
| ((value: StateFrom<TBehavior>) => void)
]
: [
options?: InterpreterOptions<TBehavior> &
InternalMachineImplementations<
TBehavior['__TContext'],
TBehavior['__TEvent'],
TBehavior['__TResolvedTypesMeta']
>,
observerOrListener?:
| Observer<StateFrom<TBehavior>>
| ((value: StateFrom<TBehavior>) => void)
]
: [
options?: InterpreterOptions<TBehavior>,
observerOrListener?:
| Observer<SnapshotFrom<TBehavior>>
| ((value: SnapshotFrom<TBehavior>) => void)
];

export function useActorRef<TBehavior extends AnyActorBehavior>(
machine: TBehavior,
...[options = {}, observerOrListener]: RestParams<TBehavior>
): ActorRefFrom<TBehavior> {
const service = useIdleInterpreter(machine, options);

useEffect(() => {
if (!observerOrListener) {
return;
}
let sub = service.subscribe(toObserver(observerOrListener));
return () => {
sub.unsubscribe();
};
}, [observerOrListener]);

useEffect(() => {
service.start();

return () => {
service.stop();
service.status = InterpreterStatus.NotStarted;
(service as any)._initState();
};
}, []);

return service as any;
}

0 comments on commit 42df9a5

Please sign in to comment.