@xstate/react 4.0.0-beta.10 → 4.0.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,10 @@ export declare function createActorContext<TLogic extends AnyActorLogic>(actorLo
7
7
  Provider: (props: {
8
8
  children: React.ReactNode;
9
9
  options?: ActorOptions<TLogic>;
10
+ /**
11
+ * @deprecated Use `logic` instead.
12
+ */
13
+ machine?: never;
10
14
  } & (TLogic extends AnyStateMachine ? AreAllImplementationsAssumedToBeProvided<TLogic['__TResolvedTypesMeta']> extends true ? {
11
15
  logic?: TLogic;
12
16
  } : {
@@ -1,14 +1,3 @@
1
- import { AnyActorLogic, AnyActor, AnyStateMachine, AreAllImplementationsAssumedToBeProvided, InternalMachineImplementations, ActorRefFrom, ActorOptions, Observer, StateFrom, SnapshotFrom, TODO } from 'xstate';
2
- export declare function useIdleInterpreter(machine: AnyActorLogic, options: Partial<ActorOptions<AnyActorLogic>>): AnyActor;
3
- type RestParams<TLogic extends AnyActorLogic> = TLogic extends AnyStateMachine ? AreAllImplementationsAssumedToBeProvided<TLogic['__TResolvedTypesMeta']> extends false ? [
4
- options: ActorOptions<TLogic> & InternalMachineImplementations<TLogic['__TContext'], TLogic['__TEvent'], TODO, TODO, TODO, TLogic['__TResolvedTypesMeta'], true>,
5
- observerOrListener?: Observer<StateFrom<TLogic>> | ((value: StateFrom<TLogic>) => void)
6
- ] : [
7
- options?: ActorOptions<TLogic> & InternalMachineImplementations<TLogic['__TContext'], TLogic['__TEvent'], TODO, TODO, TODO, TLogic['__TResolvedTypesMeta']>,
8
- observerOrListener?: Observer<StateFrom<TLogic>> | ((value: StateFrom<TLogic>) => void)
9
- ] : [
10
- options?: ActorOptions<TLogic>,
11
- observerOrListener?: Observer<SnapshotFrom<TLogic>> | ((value: SnapshotFrom<TLogic>) => void)
12
- ];
13
- export declare function useActorRef<TLogic extends AnyActorLogic>(machine: TLogic, ...[options, observerOrListener]: RestParams<TLogic>): ActorRefFrom<TLogic>;
14
- export {};
1
+ import { AnyActorLogic, AnyActor, ActorRefFrom, ActorOptions, Observer, SnapshotFrom } from 'xstate';
2
+ export declare function useIdleActor(logic: AnyActorLogic, options: Partial<ActorOptions<AnyActorLogic>>): AnyActor;
3
+ export declare function useActorRef<TLogic extends AnyActorLogic>(machine: TLogic, options?: ActorOptions<TLogic>, observerOrListener?: Observer<SnapshotFrom<TLogic>> | ((value: SnapshotFrom<TLogic>) => void)): ActorRefFrom<TLogic>;
@@ -31,29 +31,60 @@ function _interopNamespace(e) {
31
31
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
32
32
  var useIsomorphicLayoutEffect__default = /*#__PURE__*/_interopDefault(useIsomorphicLayoutEffect);
33
33
 
34
- function useConstant(fn) {
35
- const ref = React__namespace.useRef();
36
- if (!ref.current) {
37
- ref.current = {
38
- v: fn()
39
- };
34
+ const forEachActor = (actorRef, callback) => {
35
+ callback(actorRef);
36
+ const children = actorRef.getSnapshot().children;
37
+ if (children) {
38
+ Object.values(children).forEach(child => {
39
+ forEachActor(child, callback);
40
+ });
40
41
  }
41
- return ref.current.v;
42
+ };
43
+ function stopRootWithRehydration(actorRef) {
44
+ // persist state here in a custom way allows us to persist inline actors and to preserve actor references
45
+ // we do it to avoid setState in useEffect when the effect gets "reconnected"
46
+ // this currently only happens in Strict Effects but it simulates the Offscreen aka Activity API
47
+ // it also just allows us to end up with a somewhat more predictable behavior for the users
48
+ const persistedSnapshots = [];
49
+ forEachActor(actorRef, ref => {
50
+ persistedSnapshots.push([ref, ref.getSnapshot()]);
51
+ // muting observers allow us to avoid `useSelector` from being notified about the stopped state
52
+ // React reconnects its subscribers (from the useSyncExternalStore) on its own
53
+ // and userland subscibers should basically always do the same anyway
54
+ // as each subscription should have its own cleanup logic and that should be called each such reconnect
55
+ ref.observers = new Set();
56
+ });
57
+ actorRef.stop();
58
+ persistedSnapshots.forEach(([ref, snapshot]) => {
59
+ ref._processingStatus = 0;
60
+ ref._state = snapshot;
61
+ });
42
62
  }
43
63
 
44
- function useIdleInterpreter(machine, options) {
45
- const actorRef = useConstant(() => {
46
- return xstate.createActor(machine, options);
64
+ function useIdleActor(logic, options) {
65
+ let [[currentConfig, actorRef], setCurrent] = React.useState(() => {
66
+ const actorRef = xstate.createActor(logic, options);
67
+ return [logic.config, actorRef];
47
68
  });
69
+ if (logic.config !== currentConfig) {
70
+ const newActorRef = xstate.createActor(logic, {
71
+ ...options,
72
+ state: actorRef.getPersistedState({
73
+ __unsafeAllowInlineActors: true
74
+ })
75
+ });
76
+ setCurrent([logic.config, newActorRef]);
77
+ actorRef = newActorRef;
78
+ }
48
79
 
49
80
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
50
81
  useIsomorphicLayoutEffect__default["default"](() => {
51
- actorRef.logic.implementations = machine.implementations;
82
+ actorRef.logic.implementations = logic.implementations;
52
83
  });
53
84
  return actorRef;
54
85
  }
55
- function useActorRef(machine, ...[options = {}, observerOrListener]) {
56
- const actorRef = useIdleInterpreter(machine, options);
86
+ function useActorRef(machine, options = {}, observerOrListener) {
87
+ const actorRef = useIdleActor(machine, options);
57
88
  React.useEffect(() => {
58
89
  if (!observerOrListener) {
59
90
  return;
@@ -66,16 +97,14 @@ function useActorRef(machine, ...[options = {}, observerOrListener]) {
66
97
  React.useEffect(() => {
67
98
  actorRef.start();
68
99
  return () => {
69
- actorRef.stop();
70
- actorRef.status = xstate.ActorStatus.NotStarted;
71
- actorRef._initState();
100
+ stopRootWithRehydration(actorRef);
72
101
  };
73
- }, []);
102
+ }, [actorRef]);
74
103
  return actorRef;
75
104
  }
76
105
 
77
106
  function useActor(logic, options = {}) {
78
- const actorRef = useIdleInterpreter(logic, options);
107
+ const actorRef = useIdleActor(logic, options);
79
108
  const getSnapshot = React.useCallback(() => {
80
109
  return actorRef.getSnapshot();
81
110
  }, [actorRef]);
@@ -89,9 +118,7 @@ function useActor(logic, options = {}) {
89
118
  React.useEffect(() => {
90
119
  actorRef.start();
91
120
  return () => {
92
- actorRef.stop();
93
- actorRef.status = xstate.ActorStatus.NotStarted;
94
- actorRef._initState();
121
+ stopRootWithRehydration(actorRef);
95
122
  };
96
123
  }, [actorRef]);
97
124
  return [actorSnapshot, actorRef.send, actorRef];
@@ -31,35 +31,60 @@ function _interopNamespace(e) {
31
31
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
32
32
  var useIsomorphicLayoutEffect__default = /*#__PURE__*/_interopDefault(useIsomorphicLayoutEffect);
33
33
 
34
- function useConstant(fn) {
35
- const ref = React__namespace.useRef();
36
- if (!ref.current) {
37
- ref.current = {
38
- v: fn()
39
- };
34
+ const forEachActor = (actorRef, callback) => {
35
+ callback(actorRef);
36
+ const children = actorRef.getSnapshot().children;
37
+ if (children) {
38
+ Object.values(children).forEach(child => {
39
+ forEachActor(child, callback);
40
+ });
40
41
  }
41
- return ref.current.v;
42
+ };
43
+ function stopRootWithRehydration(actorRef) {
44
+ // persist state here in a custom way allows us to persist inline actors and to preserve actor references
45
+ // we do it to avoid setState in useEffect when the effect gets "reconnected"
46
+ // this currently only happens in Strict Effects but it simulates the Offscreen aka Activity API
47
+ // it also just allows us to end up with a somewhat more predictable behavior for the users
48
+ const persistedSnapshots = [];
49
+ forEachActor(actorRef, ref => {
50
+ persistedSnapshots.push([ref, ref.getSnapshot()]);
51
+ // muting observers allow us to avoid `useSelector` from being notified about the stopped state
52
+ // React reconnects its subscribers (from the useSyncExternalStore) on its own
53
+ // and userland subscibers should basically always do the same anyway
54
+ // as each subscription should have its own cleanup logic and that should be called each such reconnect
55
+ ref.observers = new Set();
56
+ });
57
+ actorRef.stop();
58
+ persistedSnapshots.forEach(([ref, snapshot]) => {
59
+ ref._processingStatus = 0;
60
+ ref._state = snapshot;
61
+ });
42
62
  }
43
63
 
44
- function useIdleInterpreter(machine, options) {
45
- {
46
- const [initialMachine] = React.useState(machine);
47
- if (machine.config !== initialMachine.config) {
48
- console.warn(`Actor logic has changed between renders. This is not supported and may lead to invalid snapshots.`);
49
- }
50
- }
51
- const actorRef = useConstant(() => {
52
- return xstate.createActor(machine, options);
64
+ function useIdleActor(logic, options) {
65
+ let [[currentConfig, actorRef], setCurrent] = React.useState(() => {
66
+ const actorRef = xstate.createActor(logic, options);
67
+ return [logic.config, actorRef];
53
68
  });
69
+ if (logic.config !== currentConfig) {
70
+ const newActorRef = xstate.createActor(logic, {
71
+ ...options,
72
+ state: actorRef.getPersistedState({
73
+ __unsafeAllowInlineActors: true
74
+ })
75
+ });
76
+ setCurrent([logic.config, newActorRef]);
77
+ actorRef = newActorRef;
78
+ }
54
79
 
55
80
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
56
81
  useIsomorphicLayoutEffect__default["default"](() => {
57
- actorRef.logic.implementations = machine.implementations;
82
+ actorRef.logic.implementations = logic.implementations;
58
83
  });
59
84
  return actorRef;
60
85
  }
61
- function useActorRef(machine, ...[options = {}, observerOrListener]) {
62
- const actorRef = useIdleInterpreter(machine, options);
86
+ function useActorRef(machine, options = {}, observerOrListener) {
87
+ const actorRef = useIdleActor(machine, options);
63
88
  React.useEffect(() => {
64
89
  if (!observerOrListener) {
65
90
  return;
@@ -72,11 +97,9 @@ function useActorRef(machine, ...[options = {}, observerOrListener]) {
72
97
  React.useEffect(() => {
73
98
  actorRef.start();
74
99
  return () => {
75
- actorRef.stop();
76
- actorRef.status = xstate.ActorStatus.NotStarted;
77
- actorRef._initState();
100
+ stopRootWithRehydration(actorRef);
78
101
  };
79
- }, []);
102
+ }, [actorRef]);
80
103
  return actorRef;
81
104
  }
82
105
 
@@ -84,7 +107,7 @@ function useActor(logic, options = {}) {
84
107
  if (!!logic && 'send' in logic && typeof logic.send === 'function') {
85
108
  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.`);
86
109
  }
87
- const actorRef = useIdleInterpreter(logic, options);
110
+ const actorRef = useIdleActor(logic, options);
88
111
  const getSnapshot = React.useCallback(() => {
89
112
  return actorRef.getSnapshot();
90
113
  }, [actorRef]);
@@ -98,9 +121,7 @@ function useActor(logic, options = {}) {
98
121
  React.useEffect(() => {
99
122
  actorRef.start();
100
123
  return () => {
101
- actorRef.stop();
102
- actorRef.status = xstate.ActorStatus.NotStarted;
103
- actorRef._initState();
124
+ stopRootWithRehydration(actorRef);
104
125
  };
105
126
  }, [actorRef]);
106
127
  return [actorSnapshot, actorRef.send, actorRef];
@@ -1,39 +1,64 @@
1
1
  import * as React from 'react';
2
2
  import { useEffect, useState, useCallback } from 'react';
3
3
  import { useSyncExternalStore } from 'use-sync-external-store/shim';
4
- import { toObserver, ActorStatus, createActor } from 'xstate';
4
+ import { toObserver, createActor } from 'xstate';
5
5
  import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect';
6
6
  import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';
7
7
 
8
- function useConstant(fn) {
9
- const ref = React.useRef();
10
- if (!ref.current) {
11
- ref.current = {
12
- v: fn()
13
- };
8
+ const forEachActor = (actorRef, callback) => {
9
+ callback(actorRef);
10
+ const children = actorRef.getSnapshot().children;
11
+ if (children) {
12
+ Object.values(children).forEach(child => {
13
+ forEachActor(child, callback);
14
+ });
14
15
  }
15
- return ref.current.v;
16
+ };
17
+ function stopRootWithRehydration(actorRef) {
18
+ // persist state here in a custom way allows us to persist inline actors and to preserve actor references
19
+ // we do it to avoid setState in useEffect when the effect gets "reconnected"
20
+ // this currently only happens in Strict Effects but it simulates the Offscreen aka Activity API
21
+ // it also just allows us to end up with a somewhat more predictable behavior for the users
22
+ const persistedSnapshots = [];
23
+ forEachActor(actorRef, ref => {
24
+ persistedSnapshots.push([ref, ref.getSnapshot()]);
25
+ // muting observers allow us to avoid `useSelector` from being notified about the stopped state
26
+ // React reconnects its subscribers (from the useSyncExternalStore) on its own
27
+ // and userland subscibers should basically always do the same anyway
28
+ // as each subscription should have its own cleanup logic and that should be called each such reconnect
29
+ ref.observers = new Set();
30
+ });
31
+ actorRef.stop();
32
+ persistedSnapshots.forEach(([ref, snapshot]) => {
33
+ ref._processingStatus = 0;
34
+ ref._state = snapshot;
35
+ });
16
36
  }
17
37
 
18
- function useIdleInterpreter(machine, options) {
19
- {
20
- const [initialMachine] = useState(machine);
21
- if (machine.config !== initialMachine.config) {
22
- console.warn(`Actor logic has changed between renders. This is not supported and may lead to invalid snapshots.`);
23
- }
24
- }
25
- const actorRef = useConstant(() => {
26
- return createActor(machine, options);
38
+ function useIdleActor(logic, options) {
39
+ let [[currentConfig, actorRef], setCurrent] = useState(() => {
40
+ const actorRef = createActor(logic, options);
41
+ return [logic.config, actorRef];
27
42
  });
43
+ if (logic.config !== currentConfig) {
44
+ const newActorRef = createActor(logic, {
45
+ ...options,
46
+ state: actorRef.getPersistedState({
47
+ __unsafeAllowInlineActors: true
48
+ })
49
+ });
50
+ setCurrent([logic.config, newActorRef]);
51
+ actorRef = newActorRef;
52
+ }
28
53
 
29
54
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
30
55
  useIsomorphicLayoutEffect(() => {
31
- actorRef.logic.implementations = machine.implementations;
56
+ actorRef.logic.implementations = logic.implementations;
32
57
  });
33
58
  return actorRef;
34
59
  }
35
- function useActorRef(machine, ...[options = {}, observerOrListener]) {
36
- const actorRef = useIdleInterpreter(machine, options);
60
+ function useActorRef(machine, options = {}, observerOrListener) {
61
+ const actorRef = useIdleActor(machine, options);
37
62
  useEffect(() => {
38
63
  if (!observerOrListener) {
39
64
  return;
@@ -46,11 +71,9 @@ function useActorRef(machine, ...[options = {}, observerOrListener]) {
46
71
  useEffect(() => {
47
72
  actorRef.start();
48
73
  return () => {
49
- actorRef.stop();
50
- actorRef.status = ActorStatus.NotStarted;
51
- actorRef._initState();
74
+ stopRootWithRehydration(actorRef);
52
75
  };
53
- }, []);
76
+ }, [actorRef]);
54
77
  return actorRef;
55
78
  }
56
79
 
@@ -58,7 +81,7 @@ function useActor(logic, options = {}) {
58
81
  if (!!logic && 'send' in logic && typeof logic.send === 'function') {
59
82
  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.`);
60
83
  }
61
- const actorRef = useIdleInterpreter(logic, options);
84
+ const actorRef = useIdleActor(logic, options);
62
85
  const getSnapshot = useCallback(() => {
63
86
  return actorRef.getSnapshot();
64
87
  }, [actorRef]);
@@ -72,9 +95,7 @@ function useActor(logic, options = {}) {
72
95
  useEffect(() => {
73
96
  actorRef.start();
74
97
  return () => {
75
- actorRef.stop();
76
- actorRef.status = ActorStatus.NotStarted;
77
- actorRef._initState();
98
+ stopRootWithRehydration(actorRef);
78
99
  };
79
100
  }, [actorRef]);
80
101
  return [actorSnapshot, actorRef.send, actorRef];
@@ -1,33 +1,64 @@
1
1
  import * as React from 'react';
2
- import { useEffect, useCallback } from 'react';
2
+ import { useEffect, useState, useCallback } from 'react';
3
3
  import { useSyncExternalStore } from 'use-sync-external-store/shim';
4
- import { toObserver, ActorStatus, createActor } from 'xstate';
4
+ import { toObserver, createActor } from 'xstate';
5
5
  import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect';
6
6
  import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';
7
7
 
8
- function useConstant(fn) {
9
- const ref = React.useRef();
10
- if (!ref.current) {
11
- ref.current = {
12
- v: fn()
13
- };
8
+ const forEachActor = (actorRef, callback) => {
9
+ callback(actorRef);
10
+ const children = actorRef.getSnapshot().children;
11
+ if (children) {
12
+ Object.values(children).forEach(child => {
13
+ forEachActor(child, callback);
14
+ });
14
15
  }
15
- return ref.current.v;
16
+ };
17
+ function stopRootWithRehydration(actorRef) {
18
+ // persist state here in a custom way allows us to persist inline actors and to preserve actor references
19
+ // we do it to avoid setState in useEffect when the effect gets "reconnected"
20
+ // this currently only happens in Strict Effects but it simulates the Offscreen aka Activity API
21
+ // it also just allows us to end up with a somewhat more predictable behavior for the users
22
+ const persistedSnapshots = [];
23
+ forEachActor(actorRef, ref => {
24
+ persistedSnapshots.push([ref, ref.getSnapshot()]);
25
+ // muting observers allow us to avoid `useSelector` from being notified about the stopped state
26
+ // React reconnects its subscribers (from the useSyncExternalStore) on its own
27
+ // and userland subscibers should basically always do the same anyway
28
+ // as each subscription should have its own cleanup logic and that should be called each such reconnect
29
+ ref.observers = new Set();
30
+ });
31
+ actorRef.stop();
32
+ persistedSnapshots.forEach(([ref, snapshot]) => {
33
+ ref._processingStatus = 0;
34
+ ref._state = snapshot;
35
+ });
16
36
  }
17
37
 
18
- function useIdleInterpreter(machine, options) {
19
- const actorRef = useConstant(() => {
20
- return createActor(machine, options);
38
+ function useIdleActor(logic, options) {
39
+ let [[currentConfig, actorRef], setCurrent] = useState(() => {
40
+ const actorRef = createActor(logic, options);
41
+ return [logic.config, actorRef];
21
42
  });
43
+ if (logic.config !== currentConfig) {
44
+ const newActorRef = createActor(logic, {
45
+ ...options,
46
+ state: actorRef.getPersistedState({
47
+ __unsafeAllowInlineActors: true
48
+ })
49
+ });
50
+ setCurrent([logic.config, newActorRef]);
51
+ actorRef = newActorRef;
52
+ }
22
53
 
23
54
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
24
55
  useIsomorphicLayoutEffect(() => {
25
- actorRef.logic.implementations = machine.implementations;
56
+ actorRef.logic.implementations = logic.implementations;
26
57
  });
27
58
  return actorRef;
28
59
  }
29
- function useActorRef(machine, ...[options = {}, observerOrListener]) {
30
- const actorRef = useIdleInterpreter(machine, options);
60
+ function useActorRef(machine, options = {}, observerOrListener) {
61
+ const actorRef = useIdleActor(machine, options);
31
62
  useEffect(() => {
32
63
  if (!observerOrListener) {
33
64
  return;
@@ -40,16 +71,14 @@ function useActorRef(machine, ...[options = {}, observerOrListener]) {
40
71
  useEffect(() => {
41
72
  actorRef.start();
42
73
  return () => {
43
- actorRef.stop();
44
- actorRef.status = ActorStatus.NotStarted;
45
- actorRef._initState();
74
+ stopRootWithRehydration(actorRef);
46
75
  };
47
- }, []);
76
+ }, [actorRef]);
48
77
  return actorRef;
49
78
  }
50
79
 
51
80
  function useActor(logic, options = {}) {
52
- const actorRef = useIdleInterpreter(logic, options);
81
+ const actorRef = useIdleActor(logic, options);
53
82
  const getSnapshot = useCallback(() => {
54
83
  return actorRef.getSnapshot();
55
84
  }, [actorRef]);
@@ -63,9 +92,7 @@ function useActor(logic, options = {}) {
63
92
  useEffect(() => {
64
93
  actorRef.start();
65
94
  return () => {
66
- actorRef.stop();
67
- actorRef.status = ActorStatus.NotStarted;
68
- actorRef._initState();
95
+ stopRootWithRehydration(actorRef);
69
96
  };
70
97
  }, [actorRef]);
71
98
  return [actorSnapshot, actorRef.send, actorRef];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xstate/react",
3
- "version": "4.0.0-beta.10",
3
+ "version": "4.0.0-beta.11",
4
4
  "description": "XState tools for React",
5
5
  "keywords": [
6
6
  "state",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "peerDependencies": {
57
57
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
58
- "xstate": "^5.0.0-beta.29"
58
+ "xstate": "^5.0.0-beta.41"
59
59
  },
60
60
  "peerDependenciesMeta": {
61
61
  "xstate": {
@@ -76,6 +76,6 @@
76
76
  "jsdom-global": "^3.0.2",
77
77
  "react": "^18.0.0",
78
78
  "react-dom": "^18.0.0",
79
- "xstate": "5.0.0-beta.29"
79
+ "xstate": "5.0.0-beta.41"
80
80
  }
81
81
  }