Skip to content

Commit

Permalink
Update @xstate/svelte (#4507)
Browse files Browse the repository at this point in the history
* Update `@xstate/svelte`

* prefer useActor

* tweak things

* fixed type errors

* yet another type error

* changesets
  • Loading branch information
Andarist authored Nov 30, 2023
1 parent ec75859 commit 9ea542c
Show file tree
Hide file tree
Showing 19 changed files with 137 additions and 119 deletions.
15 changes: 15 additions & 0 deletions .changeset/chilly-fireants-likes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@xstate/svelte': major
---

The `useMachine(machine)` hook now returns `{ snapshot, send, actorRef }` instead of `{ state, send, service }`:

```diff
const {
- state,
+ snapshot,
send,
- service
+ actorRef
} = useMachine(machine);
```
6 changes: 3 additions & 3 deletions .changeset/chilly-fireants-love.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
'@xstate/vue': major
---

The `useMachine(machine)` hook now returns `{ snapshot, send, service }` instead of `{ state, send, actorRef }`:
The `useMachine(machine)` hook now returns `{ snapshot, send, actorRef }` instead of `{ state, send, service }`:

```diff
const {
- state,
+ snapshot,
send,
- actorRef
+ service
- service
+ actorRef
} = useMachine(machine);
```
5 changes: 5 additions & 0 deletions .changeset/wicked-radios-supply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@xstate/svelte': minor
---

The `useActorRef(logic)` and `useActor(logic)` hooks have been added.
4 changes: 2 additions & 2 deletions packages/xstate-react/src/stopRootWithRehydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ const forEachActor = (
};

export function stopRootWithRehydration(actorRef: AnyActorRef) {
// persist state here in a custom way allows us to persist inline actors and to preserve actor references
// persist snapshot here in a custom way allows us to persist inline actors and to preserve actor references
// we do it to avoid setState in useEffect when the effect gets "reconnected"
// this currently only happens in Strict Effects but it simulates the Offscreen aka Activity API
// it also just allows us to end up with a somewhat more predictable behavior for the users
const persistedSnapshots: Array<[AnyActorRef, Snapshot<unknown>]> = [];
forEachActor(actorRef, (ref) => {
persistedSnapshots.push([ref, ref.getSnapshot()]);
// muting observers allow us to avoid `useSelector` from being notified about the stopped state
// muting observers allow us to avoid `useSelector` from being notified about the stopped snapshot
// React reconnects its subscribers (from the useSyncExternalStore) on its own
// and userland subscibers should basically always do the same anyway
// as each subscription should have its own cleanup logic and that should be called each such reconnect
Expand Down
4 changes: 2 additions & 2 deletions packages/xstate-react/src/useActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
ActorOptions,
SnapshotFrom
} from 'xstate';
import { useIdleActor } from './useActorRef.ts';
import { useIdleActorRef } from './useActorRef.ts';
import { stopRootWithRehydration } from './stopRootWithRehydration.ts';

export function useActor<TLogic extends AnyActorLogic>(
Expand All @@ -25,7 +25,7 @@ export function useActor<TLogic extends AnyActorLogic>(
);
}

const actorRef = useIdleActor(logic, options as any);
const actorRef = useIdleActorRef(logic, options as any);

const getSnapshot = useCallback(() => {
return actorRef.getSnapshot();
Expand Down
4 changes: 2 additions & 2 deletions packages/xstate-react/src/useActorRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect';
import { stopRootWithRehydration } from './stopRootWithRehydration';

export function useIdleActor(
export function useIdleActorRef(
logic: AnyActorLogic,
options: Partial<ActorOptions<AnyActorLogic>>
): AnyActor {
Expand Down Expand Up @@ -50,7 +50,7 @@ export function useActorRef<TLogic extends AnyActorLogic>(
| Observer<SnapshotFrom<TLogic>>
| ((value: SnapshotFrom<TLogic>) => void)
): ActorRefFrom<TLogic> {
const actorRef = useIdleActor(machine, options);
const actorRef = useIdleActorRef(machine, options);

useEffect(() => {
if (!observerOrListener) {
Expand Down
2 changes: 2 additions & 0 deletions packages/xstate-svelte/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { useActor } from './useActor.ts';
export { useActorRef } from './useActorRef.ts';
export { useMachine } from './useMachine.ts';
export { useSelector } from './useSelector.ts';
34 changes: 34 additions & 0 deletions packages/xstate-svelte/src/useActor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Readable, readable } from 'svelte/store';
import {
ActorOptions,
AnyActorLogic,
ActorRefFrom,
EventFrom,
SnapshotFrom,
AnyActorRef
} from 'xstate';
import { useActorRef } from './useActorRef';

export function useActor<TLogic extends AnyActorLogic>(
logic: TLogic,
options?: ActorOptions<TLogic>
): {
snapshot: Readable<SnapshotFrom<TLogic>>;
send: (event: EventFrom<TLogic>) => void;
actorRef: ActorRefFrom<TLogic>;
} {
const actorRef = useActorRef(logic, options) as AnyActorRef;

let currentSnapshot = actorRef.getSnapshot();

const snapshot = readable(currentSnapshot, (set) => {
return actorRef.subscribe((nextSnapshot) => {
if (currentSnapshot !== nextSnapshot) {
currentSnapshot = nextSnapshot;
set(currentSnapshot);
}
}).unsubscribe;
});

return { snapshot, send: actorRef.send, actorRef } as any;
}
11 changes: 11 additions & 0 deletions packages/xstate-svelte/src/useActorRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { onDestroy } from 'svelte';
import { ActorOptions, ActorRefFrom, AnyActorLogic, createActor } from 'xstate';

export function useActorRef<TLogic extends AnyActorLogic>(
logic: TLogic,
options?: ActorOptions<TLogic>
): ActorRefFrom<TLogic> {
const actorRef = createActor(logic as any, options).start();
onDestroy(() => actorRef.stop());
return actorRef as any;
}
69 changes: 7 additions & 62 deletions packages/xstate-svelte/src/useMachine.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,21 @@
import { onDestroy } from 'svelte';
import { Readable, readable } from 'svelte/store';
import {
AnyStateMachine,
AreAllImplementationsAssumedToBeProvided,
InternalMachineImplementations,
createActor,
ActorOptions,
StateFrom,
Actor,
ContextFrom
ActorOptions
} from 'xstate';

type Prop<T, K> = K extends keyof T ? T[K] : never;
import { useActor } from './useActor';

type RestParams<TMachine extends AnyStateMachine> =
AreAllImplementationsAssumedToBeProvided<
TMachine['__TResolvedTypesMeta']
> extends false
? [
options: ActorOptions<TMachine> &
InternalMachineImplementations<
ContextFrom<TMachine>,
TMachine['__TResolvedTypesMeta'],
true
>
]
: [
options?: ActorOptions<TMachine> &
InternalMachineImplementations<
ContextFrom<TMachine>,
TMachine['__TResolvedTypesMeta']
>
];

type UseMachineReturn<
TMachine extends AnyStateMachine,
TInterpreter = Actor<TMachine>
> = {
state: Readable<StateFrom<TMachine>>;
send: Prop<TInterpreter, 'send'>;
service: TInterpreter;
};
? [options: ActorOptions<TMachine>]
: [options?: ActorOptions<TMachine>];

/** @deprecated */
export function useMachine<TMachine extends AnyStateMachine>(
machine: TMachine,
...[options = {}]: RestParams<TMachine>
): UseMachineReturn<TMachine> {
const { guards, actions, actors, delays, ...interpreterOptions } = options;

const machineConfig = {
guards,
actions,
actors,
delays
};

const resolvedMachine = machine.provide(machineConfig as any);

const service = createActor(resolvedMachine, interpreterOptions).start();

onDestroy(() => service.stop());

let snapshot = service.getSnapshot();

const state = readable(snapshot, (set) => {
return service.subscribe((nextSnapshot) => {
if (snapshot !== nextSnapshot) {
snapshot = nextSnapshot;
set(snapshot);
}
}).unsubscribe;
});

return { state, send: service.send, service } as any;
) {
return useActor(machine, options);
}
4 changes: 2 additions & 2 deletions packages/xstate-svelte/src/useSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export const useSelector = <TActor extends ActorRef<any, any>, T>(
let prevSelected = selector(actor.getSnapshot());

const selected = readable(prevSelected, (set) => {
const onNext = (state: SnapshotFrom<TActor>) => {
const nextSelected = selector(state);
const onNext = (snapshot: SnapshotFrom<TActor>) => {
const nextSelected = selector(snapshot);
if (!compare(prevSelected, nextSelected)) {
prevSelected = nextSelected;
set(nextSelected);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
<script lang="ts">
export let persistedState: AnyMachineSnapshot | undefined = undefined;
import { useMachine } from '@xstate/svelte';
import { useActor } from '@xstate/svelte';
import { fetchMachine } from './fetchMachine.ts';
import type { AnyMachineSnapshot } from 'xstate';
import { fromPromise } from 'xstate/actors';
const onFetch = () =>
new Promise<string>((res) => setTimeout(() => res('some data'), 50));
const { state, send } = useMachine(fetchMachine, {
state: persistedState,
actors: {
fetchData: fromPromise(onFetch)
const { snapshot, send } = useActor(
fetchMachine.provide({
actors: {
fetchData: fromPromise(onFetch)
}
}),
{
state: persistedState
}
});
);
</script>

<div>
{#if $state.matches('idle')}
{#if $snapshot.matches('idle')}
<button on:click={() => send({ type: 'FETCH' })}>Fetch</button>
{:else if $state.matches('loading')}
{:else if $snapshot.matches('loading')}
<div>Loading...</div>
{:else if $state.matches('success')}
{:else if $snapshot.matches('success')}
<div>
Success! Data:
<div data-testid="data">{$state.context.data}</div>
<div data-testid="data">{$snapshot.context.data}</div>
</div>
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { useMachine } from '@xstate/svelte';
import UseMachineNonPersistentSubscriptionChild from './UseMachineNonPersistentSubscriptionChild.svelte';
import { useActor } from '@xstate/svelte';
import UseActorNonPersistentSubscriptionChild from './UseActorNonPersistentSubscriptionChild.svelte';
import { assign, createMachine } from 'xstate';
let visible = true;
Expand All @@ -18,13 +18,13 @@
}
});
const { state, send } = useMachine(machine);
const { snapshot, send } = useActor(machine);
</script>

<div>
<button type="button" on:click={() => (visible = !visible)}>Toggle</button>
{#if visible}
<!-- inlined version of this doesn't unsubscribe from the store when the content gets hidden, so we need to keep this in a separate component -->
<UseMachineNonPersistentSubscriptionChild {send} {state} />
<UseActorNonPersistentSubscriptionChild {send} {snapshot} />
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script>
export let send;
export let state;
export let snapshot;
</script>

<div>
<div data-testid="count">{$state.context.count}</div>
<div data-testid="count">{$snapshot.context.count}</div>
<button
type="button"
on:click={() => {
Expand Down
17 changes: 9 additions & 8 deletions packages/xstate-svelte/test/UseSelector.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { createActor, createMachine, assign } from 'xstate';
import { useSelector } from '@xstate/svelte';
import { useActorRef, useSelector } from '@xstate/svelte';
const machine = createMachine({
initial: 'idle',
Expand Down Expand Up @@ -30,23 +30,24 @@
}
});
const service = createActor(machine).start();
const actorRef = useActorRef(machine);
const state = useSelector(service, (state) => state);
const count = useSelector(service, (state) => state.context.count);
const snapshot = useSelector(actorRef, (s) => s);
const count = useSelector(actorRef, (s) => s.context.count);
let withSelector = 0;
$: $count && withSelector++;
let withoutSelector = 0;
$: $state.context.count && withoutSelector++;
$: $snapshot.context.count && withoutSelector++;
</script>

<button data-testid="count" on:click={() => service.send({ type: 'INCREMENT' })}
>Increment count</button
<button
data-testid="count"
on:click={() => actorRef.send({ type: 'INCREMENT' })}>Increment count</button
>
<button
data-testid="another"
on:click={() => service.send({ type: 'INCREMENT_ANOTHER' })}
on:click={() => actorRef.send({ type: 'INCREMENT_ANOTHER' })}
>Increment another count</button
>

Expand Down
10 changes: 5 additions & 5 deletions packages/xstate-svelte/test/UseSelectorCustomFn.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { createActor, createMachine, assign } from 'xstate';
import { useSelector } from '@xstate/svelte';
import { useActorRef, useSelector } from '@xstate/svelte';
const machine = createMachine({
types: {} as {
Expand All @@ -20,10 +20,10 @@
}
});
const service = createActor(machine).start();
const actorRef = useActorRef(machine);
const name = useSelector(
service,
actorRef,
(state) => state.context.name,
(a, b) => a.toUpperCase() === b.toUpperCase()
);
Expand All @@ -32,9 +32,9 @@
<div data-testid="name">{$name}</div>
<button
data-testid="sendUpper"
on:click={() => service.send({ type: 'CHANGE', value: 'DAVID' })}
on:click={() => actorRef.send({ type: 'CHANGE', value: 'DAVID' })}
/>
<button
data-testid="sendOther"
on:click={() => service.send({ type: 'CHANGE', value: 'other' })}
on:click={() => actorRef.send({ type: 'CHANGE', value: 'other' })}
/>
Loading

0 comments on commit 9ea542c

Please sign in to comment.