Skip to content

Commit

Permalink
Tie Svelte's useMachine's lifecycle to the lifecycle of the compone…
Browse files Browse the repository at this point in the history
…nt (#3172)

* Tie Svelte's `useMachine`'s lifecycle to the lifecycle of the component

* Add test and changeset

* Fix the lifetime issue of the machine in `@xstate/svelte/fsm` too
  • Loading branch information
Andarist authored Apr 27, 2022
1 parent 8520e20 commit 390a115
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/many-onions-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@xstate/svelte': patch
---

Fixed an issue with the internal interpreter created by `useMachine` being unsubscribed when its subscribers' count went to zero. The lifetime of this interpreter should be bound to the lifetime of the component that has created it.
11 changes: 5 additions & 6 deletions packages/xstate-svelte/src/fsm.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { onDestroy } from 'svelte';
import { readable } from 'svelte/store';
import {
createMachine,
Expand Down Expand Up @@ -32,16 +33,14 @@ export function useMachine<

const service = interpret(resolvedMachine).start();

onDestroy(service.stop);

const state = readable(service.state, (set) => {
service.subscribe((state) => {
return service.subscribe((state) => {
if (state.changed) {
set(state);
}
});

return () => {
service.stop();
};
}).unsubscribe;
});

return { state, send: service.send, service };
Expand Down
11 changes: 5 additions & 6 deletions packages/xstate-svelte/src/useMachine.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { onDestroy } from 'svelte';
import { readable } from 'svelte/store';
import {
interpret,
Expand Down Expand Up @@ -64,16 +65,14 @@ export function useMachine<
rehydratedState ? new State(rehydratedState) : undefined
);

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

const state = readable(service.state, (set) => {
service.subscribe((state) => {
return service.subscribe((state) => {
if (state.changed) {
set(state);
}
});

return () => {
service.stop();
};
}).unsubscribe;
});

return { state, send: service.send, service };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script lang="ts">
export let persistedState: AnyState | undefined = undefined;
import { useMachine } from '../src';
import UseMachineNonPersistentSubcriptionChild from './UseMachineNonPersistentSubcriptionChild.svelte';
import type { AnyState } from 'xstate';
import { assign, createMachine } from 'xstate';
let visible = true;
const machine = createMachine({
context: {
count: 0
},
on: {
INC: {
actions: assign({
count: (ctx: { count: number }) => ++ctx.count
})
}
}
});
const { state, send } = useMachine(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 -->
<UseMachineNonPersistentSubcriptionChild {send} {state} />
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script>
export let send;
export let state;
</script>

<div>
<div data-testid="count">{$state.context.count}</div>
<button
type="button"
on:click={() => {
send({ type: 'INC' });
}}>Increment</button
>
</div>
25 changes: 25 additions & 0 deletions packages/xstate-svelte/test/useMachine.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { render, fireEvent } from '@testing-library/svelte';
import UseMachine from './UseMachine.svelte';
import UseMachineNonPersistentSubcription from './UseMachineNonPersistentSubcription.svelte';
import { fetchMachine } from './fetchMachine';
import { doneInvoke } from 'xstate';

Expand Down Expand Up @@ -40,4 +41,28 @@ describe('useMachine function', () => {
const dataEl = getByTestId('data');
expect(dataEl.textContent).toBe('persisted data');
});

it("should not stop the interpreter even if subscribers' count go temporarily to zero", async () => {
const { findByText, getByTestId } = render(
UseMachineNonPersistentSubcription
);
let incButton = await findByText(/Increment/);

await fireEvent.click(incButton);
await fireEvent.click(incButton);
await fireEvent.click(incButton);

expect(getByTestId('count').textContent).toBe('3');

const toggleButton = await findByText(/Toggle/);

await fireEvent.click(toggleButton);
await fireEvent.click(toggleButton);

incButton = await findByText(/Increment/);

await fireEvent.click(incButton);

expect(getByTestId('count').textContent).toBe('4');
});
});

0 comments on commit 390a115

Please sign in to comment.