@xstate/react
Advanced tools
Comparing version 1.6.1 to 1.6.2
# Changelog | ||
## 1.6.2 | ||
### Patch Changes | ||
- [#2736](https://github.com/statelyai/xstate/pull/2736) [`2246ae051`](https://github.com/statelyai/xstate/commit/2246ae051663f261b4750d7adba57f008ec28f1d) Thanks [@Andarist](https://github.com/Andarist), [@davidkpiano](https://github.com/davidkpiano), [@VanTanev](https://github.com/VanTanev)! - The `useSelector(...)` hook now works as expected when the `actor` passed in changes. The hook will properly subscribe to the new `actor` and select the desired value. See [#2702](https://github.com/statelyai/xstate/issues/2702) | ||
* [#2685](https://github.com/statelyai/xstate/pull/2685) [`469268d39`](https://github.com/statelyai/xstate/commit/469268d39fbc23996599773adfc4ca824b48585f) Thanks [@farskid](https://github.com/farskid), [@Andarist](https://github.com/Andarist)! - Fixed a regression with a development-only warning not being shown when a machine reference is updated during the hook lifecycle. This usually happens when machine options are dependent on external values and they're passed via `withConfig`. | ||
```js | ||
const machine = createMachine({ | ||
initial: 'foo', | ||
context: { id: 1 }, | ||
states: { | ||
foo: { | ||
on: { | ||
CHECK: { | ||
target: 'bar', | ||
cond: 'hasOverflown' | ||
} | ||
} | ||
}, | ||
bar: {} | ||
} | ||
}); | ||
const [id, setId] = useState(1); | ||
const [current, send] = useMachine( | ||
machine.withConfig({ | ||
guards: { | ||
hasOverflown: () => id > 1 // id is a reference to an outside value | ||
} | ||
}) | ||
); | ||
// later when id updates | ||
setId(2); | ||
// Now the reference passed to `useMachine` (the result of `machine.withConfig`) is updated but the interpreted machine stays the same. So the guard is still the previous one that got passed to the `useMachine` initially, and it closes over the stale `id`. | ||
``` | ||
## 1.6.1 | ||
@@ -4,0 +43,0 @@ |
@@ -1,2 +0,2 @@ | ||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("react"),require("xstate"),require("xstate/lib/behaviors")):"function"==typeof define&&define.amd?define(["exports","react","xstate","xstate/lib/behaviors"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).XStateReact={},t.React,t.XState,t.behaviors)}(this,(function(t,e,n,r){"use strict"; | ||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("react"),require("xstate"),require("xstate/lib/behaviors")):"function"==typeof define&&define.amd?define(["exports","react","xstate","xstate/lib/behaviors"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).XStateReact={},e.React,e.XState,e.behaviors)}(this,(function(e,t,n,r){"use strict";function u(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var i,o=u(t),c=function(){return(c=Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var u in t=arguments[n])Object.prototype.hasOwnProperty.call(t,u)&&(e[u]=t[u]);return e}).apply(this,arguments)}; | ||
/*! ***************************************************************************** | ||
@@ -15,2 +15,15 @@ Copyright (c) Microsoft Corporation. | ||
PERFORMANCE OF THIS SOFTWARE. | ||
***************************************************************************** */var u,i=function(){return(i=Object.assign||function(t){for(var e,n=1,r=arguments.length;n<r;n++)for(var u in e=arguments[n])Object.prototype.hasOwnProperty.call(e,u)&&(t[u]=e[u]);return t}).apply(this,arguments)};function o(t,e){var n="function"==typeof Symbol&&t[Symbol.iterator];if(!n)return t;var r,u,i=n.call(t),o=[];try{for(;(void 0===e||e-- >0)&&!(r=i.next()).done;)o.push(r.value)}catch(t){u={error:t}}finally{try{r&&!r.done&&(n=i.return)&&n.call(i)}finally{if(u)throw u.error}}return o}function c(t,e){for(var n=0,r=e.length,u=t.length;n<r;n++,u++)t[u]=e[n];return t}!function(t){t[t.Effect=1]="Effect",t[t.LayoutEffect=2]="LayoutEffect"}(u||(u={}));var a=e.useLayoutEffect;function f(t){var n=e.useRef();return n.current||(n.current={v:t()}),n.current.v}function s(t,e){var n,r,u=o([[],[]],2),i=u[0],c=u[1];try{for(var a=function(t){var e="function"==typeof Symbol&&Symbol.iterator,n=e&&t[e],r=0;if(n)return n.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&r>=t.length&&(t=void 0),{value:t&&t[r++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")}(t),f=a.next();!f.done;f=a.next()){var s=f.value;e(s)?i.push(s):c.push(s)}}catch(t){n={error:t}}finally{try{f&&!f.done&&(r=a.return)&&r.call(a)}finally{if(n)throw n.error}}return[i,c]}function l(t,e){(0,t.exec)(e.context,e._event.data,{action:t,state:e,_event:e._event})()}function v(t,r,v){void 0===r&&(r={});var p=f((function(){return"function"==typeof t?t():t})),d=r.context,h=r.guards,b=r.actions,y=r.activities,g=r.services,m=r.delays,x=r.state,O=function(t,e){var n={};for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&e.indexOf(r)<0&&(n[r]=t[r]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols){var u=0;for(r=Object.getOwnPropertySymbols(t);u<r.length;u++)e.indexOf(r[u])<0&&Object.prototype.propertyIsEnumerable.call(t,r[u])&&(n[r[u]]=t[r[u]])}return n}(r,["context","guards","actions","activities","services","delays","state"]),S=f((function(){var t={context:d,guards:h,actions:b,activities:y,services:g,delays:m},e=p.withConfig(t,(function(){return i(i({},p.context),d)}));return n.interpret(e,i({deferEvents:!0},O))}));return a((function(){var t;return v&&(t=S.subscribe(function(t,e,n){if("object"==typeof t)return t;var r=function(){};return{next:t,error:e||r,complete:n||r}}(v))),function(){null==t||t.unsubscribe()}}),[v]),a((function(){return S.start(x?n.State.create(x):void 0),function(){S.stop()}}),[]),a((function(){Object.assign(S.machine.options.actions,b),Object.assign(S.machine.options.guards,h),Object.assign(S.machine.options.activities,y),Object.assign(S.machine.options.services,g),Object.assign(S.machine.options.delays,m)}),[b,h,y,g,m]),function(t){var n=e.useRef([]),r=e.useRef([]);a((function(){var e=t.subscribe((function(t){var e,i;if(t.actions.length){var a=o(s(t.actions.filter((function(t){return"function"==typeof t.exec&&"__effect"in t.exec})),(function(t){return t.exec.__effect===u.Effect})),2),f=a[0],l=a[1];(e=n.current).push.apply(e,c([],o(f.map((function(e){return[e,t]}))))),(i=r.current).push.apply(i,c([],o(l.map((function(e){return[e,t]})))))}}));return function(){e.unsubscribe()}}),[]),a((function(){for(;r.current.length;){var t=o(r.current.shift(),2);l(t[0],t[1])}})),e.useEffect((function(){for(;n.current.length;){var t=o(n.current.shift(),2);l(t[0],t[1])}}))}(S),S}function p(t,e){var n=function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n];return function(){return t.apply(void 0,c([],o(e)))}};return Object.defineProperties(n,{name:{value:"effect:"+t.name},__effect:{value:e}}),n}function d(t){return"state"in t}function h(t){return"deferred"in t}var b=function(){};function y(t){return"getSnapshot"in t?t.getSnapshot():d(t)?t.state:void 0}function g(t,n){void 0===n&&(n=y);var r=e.useRef(t),u=e.useRef([]),i=o(e.useState((function(){return n(t)})),2),c=i[0],s=i[1],l=f((function(){return function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var n=t[0],i=r.current;h(i)&&i.deferred?u.current.push(n):i.send(n)}}));return a((function(){r.current=t,s(n(t));for(var e=t.subscribe({next:function(t){return s(t)},error:b,complete:b});u.current.length>0;){var i=u.current.shift();t.send(i)}return function(){e.unsubscribe()}}),[t]),[c,l]}var m=function(t,e){return t===e},x=function(t){return"state"in(n=t)&&"machine"in n?0!==("status"in(e=t)?e.status:e._status)?e.state:e.machine.initialState:d(t)?t.state:void 0;var e,n};t.asEffect=function(t){return p(t,u.Effect)},t.asLayoutEffect=function(t){return p(t,u.LayoutEffect)},t.useActor=g,t.useInterpret=v,t.useMachine=function(t,r){void 0===r&&(r={});var u=e.useCallback((function(t){var e=void 0===t.changed&&Object.keys(t.children).length;(t.changed||e)&&f(t)}),[]),i=v(t,r,u),c=o(e.useState((function(){var t=i.machine.initialState;return r.state?n.State.create(r.state):t})),2),a=c[0],f=c[1];return[a,i.send,i]},t.useSelector=function(t,n,r,u){void 0===r&&(r=m),void 0===u&&(u=x);var i=o(e.useState((function(){return n(u(t))})),2),c=i[0],a=i[1],f=e.useRef(c);return e.useEffect((function(){var e=function(t){r(f.current,t)||(a(t),f.current=t)},i=n(u(t));e(i);var o=t.subscribe((function(t){var r=n(t);e(r)}));return function(){return o.unsubscribe()}}),[n,r]),c},t.useService=function(t){return[o(g(t),1)[0],t.send]},t.useSpawn=function(t){return f((function(){return r.spawnBehavior(t)}))},Object.defineProperty(t,"__esModule",{value:!0})})); | ||
***************************************************************************** */function a(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,u,i=n.call(e),o=[];try{for(;(void 0===t||t-- >0)&&!(r=i.next()).done;)o.push(r.value)}catch(e){u={error:e}}finally{try{r&&!r.done&&(n=i.return)&&n.call(i)}finally{if(u)throw u.error}}return o}function f(e,t){for(var n=0,r=t.length,u=e.length;n<r;n++,u++)e[u]=t[n];return e}!function(e){e[e.Effect=1]="Effect",e[e.LayoutEffect=2]="LayoutEffect"}(i||(i={}));var s=t.useLayoutEffect;function l(e){var n=t.useRef();return n.current||(n.current={v:e()}),n.current.v}function v(e,t){var n,r,u=a([[],[]],2),i=u[0],o=u[1];try{for(var c=function(e){var t="function"==typeof Symbol&&Symbol.iterator,n=t&&e[t],r=0;if(n)return n.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&r>=e.length&&(e=void 0),{value:e&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}(e),f=c.next();!f.done;f=c.next()){var s=f.value;t(s)?i.push(s):o.push(s)}}catch(e){n={error:e}}finally{try{f&&!f.done&&(r=c.return)&&r.call(c)}finally{if(n)throw n.error}}return[i,o]}function b(e,t){(0,e.exec)(t.context,t._event.data,{action:e,state:t,_event:t._event})()}function p(e,r,u){void 0===r&&(r={});var o=l((function(){return"function"==typeof e?e():e})),p=r.context,d=r.guards,h=r.actions,y=r.activities,g=r.services,O=r.delays,j=r.state,m=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var u=0;for(r=Object.getOwnPropertySymbols(e);u<r.length;u++)t.indexOf(r[u])<0&&Object.prototype.propertyIsEnumerable.call(e,r[u])&&(n[r[u]]=e[r[u]])}return n}(r,["context","guards","actions","activities","services","delays","state"]),x=l((function(){var e={context:p,guards:d,actions:h,activities:y,services:g,delays:O},t=o.withConfig(e,(function(){return c(c({},o.context),p)}));return n.interpret(t,c({deferEvents:!0},m))}));return s((function(){var e;return u&&(e=x.subscribe(function(e,t,n){if("object"==typeof e)return e;var r=function(){};return{next:e,error:t||r,complete:n||r}}(u))),function(){null==e||e.unsubscribe()}}),[u]),s((function(){return x.start(j?n.State.create(j):void 0),function(){x.stop()}}),[]),s((function(){Object.assign(x.machine.options.actions,h),Object.assign(x.machine.options.guards,d),Object.assign(x.machine.options.activities,y),Object.assign(x.machine.options.services,g),Object.assign(x.machine.options.delays,O)}),[h,d,y,g,O]),function(e){var n=t.useRef([]),r=t.useRef([]);s((function(){var t=e.subscribe((function(e){var t,u;if(e.actions.length){var o=a(v(e.actions.filter((function(e){return"function"==typeof e.exec&&"__effect"in e.exec})),(function(e){return e.exec.__effect===i.Effect})),2),c=o[0],s=o[1];(t=n.current).push.apply(t,f([],a(c.map((function(t){return[t,e]}))))),(u=r.current).push.apply(u,f([],a(s.map((function(t){return[t,e]})))))}}));return function(){t.unsubscribe()}}),[]),s((function(){for(;r.current.length;){var e=a(r.current.shift(),2);b(e[0],e[1])}})),t.useEffect((function(){for(;n.current.length;){var e=a(n.current.shift(),2);b(e[0],e[1])}}))}(x),x}function d(e,t){var n=function(){for(var t=[],n=0;n<arguments.length;n++)t[n]=arguments[n];return function(){return e.apply(void 0,f([],a(t)))}};return Object.defineProperties(n,{name:{value:"effect:"+e.name},__effect:{value:t}}),n}function h(e){return"state"in e}function y(e){return"deferred"in e}var g=function(){};function O(e){return"getSnapshot"in e?e.getSnapshot():h(e)?e.state:void 0}function j(e,n){void 0===n&&(n=O);var r=t.useRef(e),u=t.useRef([]),i=a(t.useState((function(){return n(e)})),2),o=i[0],c=i[1],f=l((function(){return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=e[0],i=r.current;y(i)&&i.deferred?u.current.push(n):i.send(n)}}));return s((function(){r.current=e,c(n(e));for(var t=e.subscribe({next:function(e){return c(e)},error:g,complete:g});u.current.length>0;){var i=u.current.shift();e.send(i)}return function(){t.unsubscribe()}}),[e]),[o,f]}function m(e){var t={exports:{}};return e(t,t.exports),t.exports | ||
/* | ||
object-assign | ||
(c) Sindre Sorhus | ||
@license MIT | ||
*/}var x=Object.getOwnPropertySymbols,S=Object.prototype.hasOwnProperty,w=Object.prototype.propertyIsEnumerable;function E(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}var _=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,t){for(var n,r,u=E(e),i=1;i<arguments.length;i++){for(var o in n=Object(arguments[i]))S.call(n,o)&&(u[o]=n[o]);if(x){r=x(n);for(var c=0;c<r.length;c++)w.call(n,r[c])&&(u[r[c]]=n[r[c]])}}return u},C={useSubscription:function(e){var t=e.getCurrentValue,n=e.subscribe,r=o.default.useState((function(){return{getCurrentValue:t,subscribe:n,value:t()}}));e=r[0];var u=r[1];return r=e.value,e.getCurrentValue===t&&e.subscribe===n||(r=t(),u({getCurrentValue:t,subscribe:n,value:r})),o.default.useDebugValue(r),o.default.useEffect((function(){function e(){if(!r){var e=t();u((function(r){return r.getCurrentValue!==t||r.subscribe!==n||r.value===e?r:_({},r,{value:e})}))}}var r=!1,i=n(e);return e(),function(){r=!0,i()}}),[t,n]),r}},P=(m((function(e,t){})),m((function(e){e.exports=C}))); | ||
/** @license React vundefined | ||
* use-subscription.production.min.js | ||
* | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/var V=function(e,t){return e===t},R=function(e){return"state"in(n=e)&&"machine"in n?0!==("status"in(t=e)?t.status:t._status)?t.state:t.machine.initialState:h(e)?e.state:void 0;var t,n};e.asEffect=function(e){return d(e,i.Effect)},e.asLayoutEffect=function(e){return d(e,i.LayoutEffect)},e.useActor=j,e.useInterpret=p,e.useMachine=function(e,r){void 0===r&&(r={});var u=t.useCallback((function(e){var t=void 0===e.changed&&Object.keys(e.children).length;(e.changed||t)&&f(e)}),[]),i=p(e,r,u),o=a(t.useState((function(){var e=i.machine.initialState;return r.state?n.State.create(r.state):e})),2),c=o[0],f=o[1];return[c,i.send,i]},e.useSelector=function(e,n,r,u){void 0===r&&(r=V),void 0===u&&(u=R);var i=t.useRef(n),o=t.useMemo((function(){var t,o=u(e),c=n(o);return{getSnapshot:function(){return o},getCurrentValue:function(){return c},setCurrentValue:function(e){c=e,null==t||t()},subscribe:function(n){t=n;var u=e.subscribe((function(e){o=e;var t=i.current(e);r(c,t)||(c=t,n())}));return function(){u.unsubscribe()}}}}),[e]),c=P.useSubscription(o);if(i.current!==n){var a=n(o.getSnapshot());r(c,a)||(c=a)}return s((function(){i.current=n,o.setCurrentValue(c)})),c},e.useService=function(e){return[a(j(e),1)[0],e.send]},e.useSpawn=function(e){return l((function(){return r.spawnBehavior(e)}))},Object.defineProperty(e,"__esModule",{value:!0})})); |
@@ -65,3 +65,3 @@ var __assign = (this && this.__assign) || function () { | ||
var _a = __read(useState(machine), 1), initialMachine = _a[0]; | ||
if (machine !== initialMachine) { | ||
if (getMachine !== initialMachine) { | ||
console.warn('Machine given to `useMachine` has changed between renders. This is not supported and might lead to unexpected results.\n' + | ||
@@ -68,0 +68,0 @@ 'Please make sure that you pass the same Machine as argument each time.'); |
@@ -1,18 +0,4 @@ | ||
var __read = (this && this.__read) || function (o, n) { | ||
var m = typeof Symbol === "function" && o[Symbol.iterator]; | ||
if (!m) return o; | ||
var i = m.call(o), r, ar = [], e; | ||
try { | ||
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); | ||
} | ||
catch (error) { e = { error: error }; } | ||
finally { | ||
try { | ||
if (r && !r.done && (m = i["return"])) m.call(i); | ||
} | ||
finally { if (e) throw e.error; } | ||
} | ||
return ar; | ||
}; | ||
import { useEffect, useRef, useState } from 'react'; | ||
import { useMemo, useRef } from 'react'; | ||
import { useSubscription } from 'use-subscription'; | ||
import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect'; | ||
import { isActorWithState } from './useActor'; | ||
@@ -34,20 +20,47 @@ import { getServiceSnapshot } from './useService'; | ||
if (getSnapshot === void 0) { getSnapshot = defaultGetSnapshot; } | ||
var _a = __read(useState(function () { return selector(getSnapshot(actor)); }), 2), selected = _a[0], setSelected = _a[1]; | ||
var selectedRef = useRef(selected); | ||
useEffect(function () { | ||
var updateSelectedIfChanged = function (nextSelected) { | ||
if (!compare(selectedRef.current, nextSelected)) { | ||
setSelected(nextSelected); | ||
selectedRef.current = nextSelected; | ||
var latestSelectorRef = useRef(selector); | ||
var subscription = useMemo(function () { | ||
var snapshot = getSnapshot(actor); | ||
var current = selector(snapshot); | ||
var notifySubscriber; | ||
return { | ||
getSnapshot: function () { return snapshot; }, | ||
getCurrentValue: function () { return current; }, | ||
setCurrentValue: function (newCurrent) { | ||
current = newCurrent; | ||
notifySubscriber === null || notifySubscriber === void 0 ? void 0 : notifySubscriber(); | ||
}, | ||
subscribe: function (callback) { | ||
notifySubscriber = callback; | ||
var sub = actor.subscribe(function (emitted) { | ||
snapshot = emitted; | ||
var next = latestSelectorRef.current(emitted); | ||
if (!compare(current, next)) { | ||
current = next; | ||
callback(); | ||
} | ||
}); | ||
return function () { | ||
sub.unsubscribe(); | ||
}; | ||
} | ||
}; | ||
var initialSelected = selector(getSnapshot(actor)); | ||
updateSelectedIfChanged(initialSelected); | ||
var sub = actor.subscribe(function (emitted) { | ||
var nextSelected = selector(emitted); | ||
updateSelectedIfChanged(nextSelected); | ||
}); | ||
return function () { return sub.unsubscribe(); }; | ||
}, [selector, compare]); | ||
return selected; | ||
// intentionally omit `getSnapshot` and `compare` | ||
// - `getSnapshot`: it is only supposed to read the "initial" snapshot of an actor | ||
// - `compare`: is really supposed to be idempotent and the same throughout the lifetime of this hook (the same assumption is made in React Redux v7) | ||
}, [actor]); | ||
var currentSelected = useSubscription(subscription); | ||
if (latestSelectorRef.current !== selector) { | ||
var selected = selector(subscription.getSnapshot()); | ||
if (!compare(currentSelected, selected)) { | ||
currentSelected = selected; | ||
} | ||
} | ||
useIsomorphicLayoutEffect(function () { | ||
latestSelectorRef.current = selector; | ||
// required so we don't cause a rerender by setting state (this could create infinite rerendering loop with inline selectors) | ||
// at the same time we need to update the value within the subscription so new emits can compare against what has been returned to the user as current value | ||
subscription.setCurrentValue(currentSelected); | ||
}); | ||
return currentSelected; | ||
} |
@@ -68,3 +68,3 @@ "use strict"; | ||
var _a = __read(react_1.useState(machine), 1), initialMachine = _a[0]; | ||
if (machine !== initialMachine) { | ||
if (getMachine !== initialMachine) { | ||
console.warn('Machine given to `useMachine` has changed between renders. This is not supported and might lead to unexpected results.\n' + | ||
@@ -71,0 +71,0 @@ 'Please make sure that you pass the same Machine as argument each time.'); |
"use strict"; | ||
var __read = (this && this.__read) || function (o, n) { | ||
var m = typeof Symbol === "function" && o[Symbol.iterator]; | ||
if (!m) return o; | ||
var i = m.call(o), r, ar = [], e; | ||
try { | ||
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); | ||
} | ||
catch (error) { e = { error: error }; } | ||
finally { | ||
try { | ||
if (r && !r.done && (m = i["return"])) m.call(i); | ||
} | ||
finally { if (e) throw e.error; } | ||
} | ||
return ar; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.useSelector = void 0; | ||
var react_1 = require("react"); | ||
var use_subscription_1 = require("use-subscription"); | ||
var use_isomorphic_layout_effect_1 = require("use-isomorphic-layout-effect"); | ||
var useActor_1 = require("./useActor"); | ||
@@ -37,21 +23,48 @@ var useService_1 = require("./useService"); | ||
if (getSnapshot === void 0) { getSnapshot = defaultGetSnapshot; } | ||
var _a = __read(react_1.useState(function () { return selector(getSnapshot(actor)); }), 2), selected = _a[0], setSelected = _a[1]; | ||
var selectedRef = react_1.useRef(selected); | ||
react_1.useEffect(function () { | ||
var updateSelectedIfChanged = function (nextSelected) { | ||
if (!compare(selectedRef.current, nextSelected)) { | ||
setSelected(nextSelected); | ||
selectedRef.current = nextSelected; | ||
var latestSelectorRef = react_1.useRef(selector); | ||
var subscription = react_1.useMemo(function () { | ||
var snapshot = getSnapshot(actor); | ||
var current = selector(snapshot); | ||
var notifySubscriber; | ||
return { | ||
getSnapshot: function () { return snapshot; }, | ||
getCurrentValue: function () { return current; }, | ||
setCurrentValue: function (newCurrent) { | ||
current = newCurrent; | ||
notifySubscriber === null || notifySubscriber === void 0 ? void 0 : notifySubscriber(); | ||
}, | ||
subscribe: function (callback) { | ||
notifySubscriber = callback; | ||
var sub = actor.subscribe(function (emitted) { | ||
snapshot = emitted; | ||
var next = latestSelectorRef.current(emitted); | ||
if (!compare(current, next)) { | ||
current = next; | ||
callback(); | ||
} | ||
}); | ||
return function () { | ||
sub.unsubscribe(); | ||
}; | ||
} | ||
}; | ||
var initialSelected = selector(getSnapshot(actor)); | ||
updateSelectedIfChanged(initialSelected); | ||
var sub = actor.subscribe(function (emitted) { | ||
var nextSelected = selector(emitted); | ||
updateSelectedIfChanged(nextSelected); | ||
}); | ||
return function () { return sub.unsubscribe(); }; | ||
}, [selector, compare]); | ||
return selected; | ||
// intentionally omit `getSnapshot` and `compare` | ||
// - `getSnapshot`: it is only supposed to read the "initial" snapshot of an actor | ||
// - `compare`: is really supposed to be idempotent and the same throughout the lifetime of this hook (the same assumption is made in React Redux v7) | ||
}, [actor]); | ||
var currentSelected = use_subscription_1.useSubscription(subscription); | ||
if (latestSelectorRef.current !== selector) { | ||
var selected = selector(subscription.getSnapshot()); | ||
if (!compare(currentSelected, selected)) { | ||
currentSelected = selected; | ||
} | ||
} | ||
use_isomorphic_layout_effect_1.default(function () { | ||
latestSelectorRef.current = selector; | ||
// required so we don't cause a rerender by setting state (this could create infinite rerendering loop with inline selectors) | ||
// at the same time we need to update the value within the subscription so new emits can compare against what has been returned to the user as current value | ||
subscription.setCurrentValue(currentSelected); | ||
}); | ||
return currentSelected; | ||
} | ||
exports.useSelector = useSelector; |
{ | ||
"name": "@xstate/react", | ||
"version": "1.6.1", | ||
"version": "1.6.2", | ||
"description": "XState tools for React", | ||
@@ -42,3 +42,3 @@ "keywords": [ | ||
"test": "jest", | ||
"prepublish": "npm run build && npm run test" | ||
"prepublish": "npm run build" | ||
}, | ||
@@ -45,0 +45,0 @@ "bugs": { |
517
README.md
# @xstate/react | ||
[[toc]] | ||
This package contains utilities for using [XState](https://github.com/statelyai/xstate) with [React](https://github.com/facebook/react/). | ||
## Quick Start | ||
- [Read the full documentation in the XState docs](https://xstate.js.org/docs/packages/xstate-react/). | ||
- [Read our contribution guidelines](https://github.com/statelyai/xstate/blob/main/CONTRIBUTING.md). | ||
## Quick start | ||
1. Install `xstate` and `@xstate/react`: | ||
@@ -60,511 +63,1 @@ | ||
``` | ||
## Examples | ||
- [XState + React TodoMVC (CodeSandbox)](https://codesandbox.io/s/xstate-todomvc-33wr94qv1) | ||
## API | ||
### `useMachine(machine, options?)` | ||
A [React hook](https://reactjs.org/hooks) that interprets the given `machine` and starts a service that runs for the lifetime of the component. | ||
**Arguments** | ||
- `machine` - An [XState machine](https://xstate.js.org/docs/guides/machines.html) or a function that lazily returns a machine: | ||
```js | ||
// existing machine | ||
const [state, send] = useMachine(machine); | ||
// lazily-created machine | ||
const [state, send] = useMachine(() => | ||
createMachine({ | ||
/* ... */ | ||
}) | ||
); | ||
``` | ||
- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) and/or any of the following machine config options: `guards`, `actions`, `services`, `delays`, `immediate`, `context`, `state`. | ||
**Returns** a tuple of `[state, send, service]`: | ||
- `state` - Represents the current state of the machine as an XState `State` object. | ||
- `send` - A function that sends events to the running service. | ||
- `service` - The created service. | ||
### `useService(service)` | ||
::: warning Deprecated | ||
In the next major version, `useService(service)` will be replaced with `useActor(service)`. Prefer using the `useActor(service)` hook for services instead, since services are also actors. | ||
Also, keep in mind that only a single argument (the event object) can be sent to `send(eventObject)` from `useActor(...)`. When migrating to `useActor(...)`, refactor `send(...)` calls to use only a single event object: | ||
```diff | ||
const [state, send] = useActor(service); | ||
-send('CLICK', { x: 0, y: 3 }); | ||
+send({ type: 'CLICK', x: 0, y: 3 }); | ||
``` | ||
::: | ||
A [React hook](https://reactjs.org/hooks) that subscribes to state changes from an existing [service](https://xstate.js.org/docs/guides/interpretation.html). | ||
**Arguments** | ||
- `service` - An [XState service](https://xstate.js.org/docs/guides/interpretation.html). | ||
**Returns** a tuple of `[state, send]`: | ||
- `state` - Represents the current state of the service as an XState `State` object. | ||
- `send` - A function that sends events to the running service. | ||
### `useActor(actor, getSnapshot?)` | ||
A [React hook](https://reactjs.org/hooks) that subscribes to emitted changes from an existing [actor](https://xstate.js.org/docs/guides/actors.html). | ||
**Arguments** | ||
- `actor` - an actor-like object that contains `.send(...)` and `.subscribe(...)` methods. | ||
- `getSnapshot` - a function that should return the latest emitted value from the `actor`. | ||
- Defaults to attempting to get the `actor.state`, or returning `undefined` if that does not exist. | ||
```js | ||
const [state, send] = useActor(someSpawnedActor); | ||
// with custom actors | ||
const [state, send] = useActor(customActor, (actor) => { | ||
// implementation-specific pseudocode example: | ||
return actor.getLastEmittedValue(); | ||
}); | ||
``` | ||
### `useInterpret(machine, options?, observer?)` | ||
A React hook that returns the `service` created from the `machine` with the `options`, if specified. It also sets up a subscription to the `service` with the `observer`, if provided. | ||
_Since 1.3.0_ | ||
**Arguments** | ||
- `machine` - An [XState machine](https://xstate.js.org/docs/guides/machines.html) or a function that lazily returns a machine. | ||
- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) and/or any of the following machine config options: `guards`, `actions`, `services`, `delays`, `immediate`, `context`, `state`. | ||
- `observer` (optional) - an observer or listener that listens to state updates: | ||
- an observer (e.g., `{ next: (state) => {/* ... */} }`) | ||
- or a listener (e.g., `(state) => {/* ... */}`) | ||
```js | ||
import { useInterpret } from '@xstate/react'; | ||
import { someMachine } from '../path/to/someMachine'; | ||
const App = () => { | ||
const service = useInterpret(someMachine); | ||
// ... | ||
}; | ||
``` | ||
With options + listener: | ||
```js | ||
// ... | ||
const App = () => { | ||
const service = useInterpret( | ||
someMachine, | ||
{ | ||
actions: { | ||
/* ... */ | ||
} | ||
}, | ||
(state) => { | ||
// subscribes to state changes | ||
console.log(state); | ||
} | ||
); | ||
// ... | ||
}; | ||
``` | ||
### `useSelector(actor, selector, compare?, getSnapshot?)` | ||
A React hook that returns the selected value from the snapshot of an `actor`, such as a service. This hook will only cause a rerender if the selected value changes, as determined by the optional `compare` function. | ||
_Since 1.3.0_ | ||
**Arguments** | ||
- `actor` - a service or an actor-like object that contains `.send(...)` and `.subscribe(...)` methods. | ||
- `selector` - a function that takes in an actor's "current state" (snapshot) as an argument and returns the desired selected value. | ||
- `compare` (optional) - a function that determines if the current selected value is the same as the previous selected value. | ||
- `getSnapshot` (optional) - a function that should return the latest emitted value from the `actor`. | ||
- Defaults to attempting to get the `actor.state`, or returning `undefined` if that does not exist. Will automatically pull the state from services. | ||
```js | ||
import { useSelector } from '@xstate/react'; | ||
// tip: optimize selectors by defining them externally when possible | ||
const selectCount = (state) => state.context.count; | ||
const App = ({ service }) => { | ||
const count = useSelector(service, selectCount); | ||
// ... | ||
}; | ||
``` | ||
With `compare` function: | ||
```js | ||
// ... | ||
const selectUser = (state) => state.context.user; | ||
const compareUser = (prevUser, nextUser) => prevUser.id === nextUser.id; | ||
const App = ({ service }) => { | ||
const user = useSelector(service, selectUser, compareUser); | ||
// ... | ||
}; | ||
``` | ||
With `useInterpret(...)`: | ||
```js | ||
import { useInterpret, useSelector } from '@xstate/react'; | ||
import { someMachine } from '../path/to/someMachine'; | ||
const selectCount = (state) => state.context.count; | ||
const App = ({ service }) => { | ||
const service = useInterpret(someMachine); | ||
const count = useSelector(service, selectCount); | ||
// ... | ||
}; | ||
``` | ||
### `asEffect(action)` | ||
Ensures that the `action` is executed as an effect in `useEffect`, rather than being immediately executed. | ||
**Arguments** | ||
- `action` - An action function (e.g., `(context, event) => { alert(context.message) })`) | ||
**Returns** a special action function that wraps the original so that `useMachine` knows to execute it in `useEffect`. | ||
**Example** | ||
```jsx | ||
const machine = createMachine({ | ||
initial: 'focused', | ||
states: { | ||
focused: { | ||
entry: 'focus' | ||
} | ||
} | ||
}); | ||
const Input = () => { | ||
const inputRef = useRef(null); | ||
const [state, send] = useMachine(machine, { | ||
actions: { | ||
focus: asEffect((context, event) => { | ||
inputRef.current && inputRef.current.focus(); | ||
}) | ||
} | ||
}); | ||
return <input ref={inputRef} />; | ||
}; | ||
``` | ||
### `asLayoutEffect(action)` | ||
Ensures that the `action` is executed as an effect in `useLayoutEffect`, rather than being immediately executed. | ||
**Arguments** | ||
- `action` - An action function (e.g., `(context, event) => { alert(context.message) })`) | ||
**Returns** a special action function that wraps the original so that `useMachine` knows to execute it in `useLayoutEffect`. | ||
### `useMachine(machine)` with `@xstate/fsm` | ||
A [React hook](https://reactjs.org/hooks) that interprets the given finite state `machine` from [`@xstate/fsm`] and starts a service that runs for the lifetime of the component. | ||
This special `useMachine` hook is imported from `@xstate/react/fsm` | ||
**Arguments** | ||
- `machine` - An [XState finite state machine (FSM)](https://xstate.js.org/docs/packages/xstate-fsm/). | ||
- `options` - An optional `options` object. | ||
**Returns** a tuple of `[state, send, service]`: | ||
- `state` - Represents the current state of the machine as an `@xstate/fsm` `StateMachine.State` object. | ||
- `send` - A function that sends events to the running service. | ||
- `service` - The created `@xstate/fsm` service. | ||
**Example** | ||
```js | ||
import { useEffect } from 'react'; | ||
import { useMachine } from '@xstate/react/fsm'; | ||
import { createMachine } from '@xstate/fsm'; | ||
const context = { | ||
data: undefined | ||
}; | ||
const fetchMachine = createMachine({ | ||
id: 'fetch', | ||
initial: 'idle', | ||
context, | ||
states: { | ||
idle: { | ||
on: { FETCH: 'loading' } | ||
}, | ||
loading: { | ||
entry: ['load'], | ||
on: { | ||
RESOLVE: { | ||
target: 'success', | ||
actions: assign({ | ||
data: (context, event) => event.data | ||
}) | ||
} | ||
} | ||
}, | ||
success: {} | ||
} | ||
}); | ||
const Fetcher = ({ | ||
onFetch = () => new Promise((res) => res('some data')) | ||
}) => { | ||
const [state, send] = useMachine(fetchMachine, { | ||
actions: { | ||
load: () => { | ||
onFetch().then((res) => { | ||
send({ type: 'RESOLVE', data: res }); | ||
}); | ||
} | ||
} | ||
}); | ||
switch (state.value) { | ||
case 'idle': | ||
return <button onClick={(_) => send('FETCH')}>Fetch</button>; | ||
case 'loading': | ||
return <div>Loading...</div>; | ||
case 'success': | ||
return ( | ||
<div> | ||
Success! Data: <div data-testid="data">{state.context.data}</div> | ||
</div> | ||
); | ||
default: | ||
return null; | ||
} | ||
}; | ||
``` | ||
## Configuring Machines | ||
Existing machines can be configured by passing the machine options as the 2nd argument of `useMachine(machine, options)`. | ||
Example: the `'fetchData'` service and `'notifySuccess'` action are both configurable: | ||
```js | ||
const fetchMachine = createMachine({ | ||
id: 'fetch', | ||
initial: 'idle', | ||
context: { | ||
data: undefined, | ||
error: undefined | ||
}, | ||
states: { | ||
idle: { | ||
on: { FETCH: 'loading' } | ||
}, | ||
loading: { | ||
invoke: { | ||
src: 'fetchData', | ||
onDone: { | ||
target: 'success', | ||
actions: assign({ | ||
data: (_, event) => event.data | ||
}) | ||
}, | ||
onError: { | ||
target: 'failure', | ||
actions: assign({ | ||
error: (_, event) => event.data | ||
}) | ||
} | ||
} | ||
}, | ||
success: { | ||
entry: 'notifySuccess', | ||
type: 'final' | ||
}, | ||
failure: { | ||
on: { | ||
RETRY: 'loading' | ||
} | ||
} | ||
} | ||
}); | ||
const Fetcher = ({ onResolve }) => { | ||
const [state, send] = useMachine(fetchMachine, { | ||
actions: { | ||
notifySuccess: (ctx) => onResolve(ctx.data) | ||
}, | ||
services: { | ||
fetchData: (_, e) => | ||
fetch(`some/api/${e.query}`).then((res) => res.json()) | ||
} | ||
}); | ||
switch (state.value) { | ||
case 'idle': | ||
return ( | ||
<button onClick={() => send('FETCH', { query: 'something' })}> | ||
Search for something | ||
</button> | ||
); | ||
case 'loading': | ||
return <div>Searching...</div>; | ||
case 'success': | ||
return <div>Success! Data: {state.context.data}</div>; | ||
case 'failure': | ||
return ( | ||
<> | ||
<p>{state.context.error.message}</p> | ||
<button onClick={() => send('RETRY')}>Retry</button> | ||
</> | ||
); | ||
default: | ||
return null; | ||
} | ||
}; | ||
``` | ||
## Matching States | ||
When using [hierarchical](https://xstate.js.org/docs/guides/hierarchical.html) and [parallel](https://xstate.js.org/docs/guides/parallel.html) machines, the state values will be objects, not strings. In this case, it is best to use [`state.matches(...)`](https://xstate.js.org/docs/guides/states.html#state-methods-and-getters). | ||
We can do this with `if/else if/else` blocks: | ||
```js | ||
// ... | ||
if (state.matches('idle')) { | ||
return /* ... */; | ||
} else if (state.matches({ loading: 'user' })) { | ||
return /* ... */; | ||
} else if (state.matches({ loading: 'friends' })) { | ||
return /* ... */; | ||
} else { | ||
return null; | ||
} | ||
``` | ||
We can also continue to use `switch`, but we must make an adjustment to our approach. By setting the expression of the `switch` to `true`, we can use [`state.matches(...)`](https://xstate.js.org/docs/guides/states.html#state-methods-and-getters) as a predicate in each `case`: | ||
```js | ||
switch (true) { | ||
case state.matches('idle'): | ||
return /* ... */; | ||
case state.matches({ loading: 'user' }): | ||
return /* ... */; | ||
case state.matches({ loading: 'friends' }): | ||
return /* ... */; | ||
default: | ||
return null; | ||
} | ||
``` | ||
A ternary statement can also be considered, especially within rendered JSX: | ||
```jsx | ||
const Loader = () => { | ||
const [state, send] = useMachine(/* ... */); | ||
return ( | ||
<div> | ||
{state.matches('idle') ? ( | ||
<Loader.Idle /> | ||
) : state.matches({ loading: 'user' }) ? ( | ||
<Loader.LoadingUser /> | ||
) : state.matches({ loading: 'friends' }) ? ( | ||
<Loader.LoadingFriends /> | ||
) : null} | ||
</div> | ||
); | ||
}; | ||
``` | ||
## Persisted and Rehydrated State | ||
You can persist and rehydrate state with `useMachine(...)` via `options.state`: | ||
```js | ||
// ... | ||
// Get the persisted state config object from somewhere, e.g. localStorage | ||
const persistedState = JSON.parse(localStorage.getItem('some-persisted-state-key')) || someMachine.initialState; | ||
const App = () => { | ||
const [state, send] = useMachine(someMachine, { | ||
state: persistedState // provide persisted state config object here | ||
}); | ||
// state will initially be that persisted state, not the machine's initialState | ||
return (/* ... */) | ||
} | ||
``` | ||
## Services | ||
The `service` created in `useMachine(machine)` can be referenced as the third returned value: | ||
```js | ||
// vvvvvvv | ||
const [state, send, service] = useMachine(someMachine); | ||
``` | ||
You can subscribe to that service's state changes with the [`useEffect` hook](https://reactjs.org/docs/hooks-effect.html): | ||
```js | ||
// ... | ||
useEffect(() => { | ||
const subscription = service.subscribe((state) => { | ||
// simple state logging | ||
console.log(state); | ||
}); | ||
return subscription.unsubscribe; | ||
}, [service]); // note: service should never change | ||
``` | ||
## Migration from 0.x | ||
- For spawned actors created using `invoke` or `spawn(...)`, use the `useActor()` hook instead of `useService()`: | ||
```diff | ||
-import { useService } from '@xstate/react'; | ||
+import { useActor } from '@xstate/react'; | ||
-const [state, send] = useService(someActor); | ||
+const [state, send] = useActor(someActor); | ||
``` | ||
## Resources | ||
[State Machines in React](https://gedd.ski/post/state-machines-in-react/) |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
1642
102436
63
1