@xstate/react
Advanced tools
Comparing version 1.0.0-rc.4 to 1.0.0-rc.5
@@ -5,2 +5,36 @@ # Changelog | ||
## [1.0.0-rc.5] | ||
- You can now schedule actions in `useEffect` or `useLayoutEffect` via: | ||
- `asEffect` - queues the action to be executed in `useEffect` | ||
- `asLayoutEffect` - queues the action to be executed in `useLayoutEffect` | ||
```jsx | ||
import { createMachine } from 'xstate'; | ||
import { useMachine, asEffect } from '@xstate/react'; | ||
const machine = createMachine({ | ||
initial: 'focused', | ||
states: { | ||
focused: { | ||
entry: 'focus' | ||
} | ||
} | ||
}); | ||
const Input = () => { | ||
const inputRef = useRef(null); | ||
const [state, send] = useMachine(machine, { | ||
actions: { | ||
focus: asEffect(() => { | ||
inputRef.current && inputRef.current.focus(); | ||
}) | ||
} | ||
}); | ||
return <input ref={inputRef} />; | ||
}; | ||
``` | ||
## [0.8.1] | ||
@@ -7,0 +41,0 @@ |
@@ -0,4 +1,3 @@ | ||
export { useMachine } from './useMachine'; | ||
export { useService } from './useService'; | ||
export { useMachine } from './useMachine'; | ||
export { useActor } from './useActor'; | ||
//# sourceMappingURL=index.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var useMachine_1 = require("./useMachine"); | ||
exports.useMachine = useMachine_1.useMachine; | ||
var useService_1 = require("./useService"); | ||
exports.useService = useService_1.useService; | ||
var useMachine_1 = require("./useMachine"); | ||
exports.useMachine = useMachine_1.useMachine; | ||
var useActor_1 = require("./useActor"); | ||
exports.useActor = useActor_1.useActor; |
@@ -1,4 +0,4 @@ | ||
import { ActorRef, Sender } from './ActorRef'; | ||
import { EventObject } from 'xstate'; | ||
export declare function useActor<TCurrent, TEvent extends EventObject>(actorRef: ActorRef<TCurrent, TEvent>): [TCurrent, Sender<TEvent>]; | ||
import { Sender, ActorRefLike } from './types'; | ||
import { EventObject, Actor } from 'xstate'; | ||
export declare function useActor<TEvent extends EventObject, TEmitted = any>(actorLike: ActorRefLike<TEvent, TEmitted> | Actor): [TEmitted, Sender<TEvent>]; | ||
//# sourceMappingURL=useActor.d.ts.map |
"use strict"; | ||
var __assign = (this && this.__assign) || function () { | ||
__assign = Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
var __read = (this && this.__read) || function (o, n) { | ||
@@ -20,14 +31,43 @@ var m = typeof Symbol === "function" && o[Symbol.iterator]; | ||
var react_1 = require("react"); | ||
function useActor(actorRef) { | ||
var _a = __read(react_1.useState(actorRef.current), 2), current = _a[0], setCurrent = _a[1]; | ||
var send = actorRef.send; | ||
var xstate_1 = require("xstate"); | ||
var useService_1 = require("./useService"); | ||
function resolveActor(actorLike) { | ||
if (actorLike instanceof xstate_1.Interpreter) { | ||
return useService_1.fromService(actorLike); | ||
} | ||
if (!('current' in actorLike)) { | ||
return __assign(__assign({ stop: function () { | ||
/* do nothing */ | ||
} }, actorLike), { subscribe: actorLike.subscribe, name: actorLike.id, current: actorLike.state }); | ||
} | ||
return actorLike; | ||
} | ||
function useActor(actorLike) { | ||
var actor = react_1.useMemo(function () { return resolveActor(actorLike); }, [actorLike]); | ||
var deferredEventsRef = react_1.useRef([]); | ||
var _a = __read(react_1.useState(actor.current), 2), current = _a[0], setCurrent = _a[1]; | ||
var send = react_1.useCallback(function (event) { | ||
// If the previous actor is a deferred actor, | ||
// queue the events so that they can be replayed | ||
// on the non-deferred actor. | ||
if (actor.deferred) { | ||
deferredEventsRef.current.push(event); | ||
} | ||
else { | ||
actor.send(event); | ||
} | ||
}, [actor]); | ||
react_1.useEffect(function () { | ||
var sub = actorRef.subscribe(function (latest) { | ||
// this will be called with the current value immediately | ||
setCurrent(latest); | ||
}); | ||
return function () { return sub.unsubscribe(); }; | ||
}, [actorRef]); | ||
var subscription = actor.subscribe(setCurrent); | ||
// Dequeue deferred events from the previous deferred actor | ||
while (deferredEventsRef.current.length > 0) { | ||
var deferredEvent = deferredEventsRef.current.shift(); | ||
actor.send(deferredEvent); | ||
} | ||
return function () { | ||
subscription.unsubscribe(); | ||
}; | ||
}, [actor]); | ||
return [current, send]; | ||
} | ||
exports.useActor = useActor; |
@@ -1,2 +0,16 @@ | ||
import { EventObject, StateMachine, State, Interpreter, InterpreterOptions, MachineOptions, Typestate, StateConfig } from 'xstate'; | ||
import { EventObject, StateMachine, State, Interpreter, InterpreterOptions, MachineOptions, StateConfig, Typestate, ActionObject, ActionFunction, ActionMeta } from 'xstate'; | ||
declare enum ReactEffectType { | ||
Effect = 1, | ||
LayoutEffect = 2 | ||
} | ||
export interface ReactActionFunction<TContext, TEvent extends EventObject> { | ||
(context: TContext, event: TEvent, meta: ActionMeta<TContext, TEvent>): () => void; | ||
__effect: ReactEffectType; | ||
} | ||
export interface ReactActionObject<TContext, TEvent extends EventObject> extends ActionObject<TContext, TEvent> { | ||
exec: ReactActionFunction<TContext, TEvent>; | ||
} | ||
export declare function asEffect<TContext, TEvent extends EventObject>(exec: ActionFunction<TContext, TEvent>): ReactActionFunction<TContext, TEvent>; | ||
export declare function asLayoutEffect<TContext, TEvent extends EventObject>(exec: ActionFunction<TContext, TEvent>): ReactActionFunction<TContext, TEvent>; | ||
export declare type ActionStateTuple<TContext, TEvent extends EventObject> = [ReactActionObject<TContext, TEvent>, State<TContext, TEvent>]; | ||
interface UseMachineOptions<TContext, TEvent extends EventObject> { | ||
@@ -3,0 +17,0 @@ /** |
@@ -40,6 +40,51 @@ "use strict"; | ||
}; | ||
var __spread = (this && this.__spread) || function () { | ||
for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); | ||
return ar; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var react_1 = require("react"); | ||
var use_isomorphic_layout_effect_1 = require("use-isomorphic-layout-effect"); | ||
var xstate_1 = require("xstate"); | ||
var useConstant_1 = require("./useConstant"); | ||
var utils_1 = require("./utils"); | ||
var ReactEffectType; | ||
(function (ReactEffectType) { | ||
ReactEffectType[ReactEffectType["Effect"] = 1] = "Effect"; | ||
ReactEffectType[ReactEffectType["LayoutEffect"] = 2] = "LayoutEffect"; | ||
})(ReactEffectType || (ReactEffectType = {})); | ||
function createReactActionFunction(exec, tag) { | ||
var effectExec = function () { | ||
var args = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
args[_i] = arguments[_i]; | ||
} | ||
// don't execute; just return | ||
return function () { | ||
return exec.apply(void 0, __spread(args)); | ||
}; | ||
}; | ||
Object.defineProperties(effectExec, { | ||
name: { value: "effect:" + exec.name }, | ||
__effect: { value: tag } | ||
}); | ||
return effectExec; | ||
} | ||
function asEffect(exec) { | ||
return createReactActionFunction(exec, ReactEffectType.Effect); | ||
} | ||
exports.asEffect = asEffect; | ||
function asLayoutEffect(exec) { | ||
return createReactActionFunction(exec, ReactEffectType.LayoutEffect); | ||
} | ||
exports.asLayoutEffect = asLayoutEffect; | ||
function executeEffect(action, state) { | ||
var exec = action.exec; | ||
var originalExec = exec(state.context, state._event.data, { | ||
action: action, | ||
state: state, | ||
_event: state._event | ||
}); | ||
originalExec(); | ||
} | ||
function useMachine(machine, options) { | ||
@@ -55,4 +100,3 @@ if (options === void 0) { options = {}; } | ||
var context = options.context, guards = options.guards, actions = options.actions, activities = options.activities, services = options.services, delays = options.delays, rehydratedState = options.state, interpreterOptions = __rest(options, ["context", "guards", "actions", "activities", "services", "delays", "state"]); | ||
// Keep a single reference to the invoked machine (the service) | ||
var service = useConstant_1.default(function () { | ||
var _b = __read(useConstant_1.default(function () { | ||
var machineConfig = { | ||
@@ -66,13 +110,47 @@ context: context, | ||
}; | ||
var createdMachine = machine.withConfig(machineConfig, __assign(__assign({}, machine.context), context)); | ||
// Ensure that actions are not executed (until useEffect() below) | ||
interpreterOptions.execute = false; | ||
return xstate_1.interpret(createdMachine, interpreterOptions).start(rehydratedState ? xstate_1.State.create(rehydratedState) : undefined); | ||
}); | ||
// Initialize the state with the initial state. | ||
var _b = __read(react_1.useState(service.state), 2), state = _b[0], setState = _b[1]; | ||
// Capture all actions (side-effects) to be executed. | ||
// These will be flushed when they are executed, and avoids the issue of batched events | ||
// sent to the interpreter, which might ignore actions. | ||
var actionStatesRef = react_1.useRef([state]); | ||
var resolvedMachine = machine.withConfig(machineConfig, __assign(__assign({}, machine.context), context)); | ||
return [ | ||
resolvedMachine, | ||
xstate_1.interpret(resolvedMachine, __assign({ deferEvents: true }, interpreterOptions)) | ||
]; | ||
}), 2), resolvedMachine = _b[0], service = _b[1]; | ||
var _c = __read(react_1.useState(function () { | ||
return rehydratedState | ||
? xstate_1.State.create(rehydratedState) | ||
: resolvedMachine.initialState; | ||
}), 2), state = _c[0], setState = _c[1]; | ||
var effectActionsRef = react_1.useRef([]); | ||
var layoutEffectActionsRef = react_1.useRef([]); | ||
use_isomorphic_layout_effect_1.default(function () { | ||
service | ||
.onTransition(function (currentState) { | ||
var _a, _b; | ||
// Only change the current state if: | ||
// - the incoming state is the "live" initial state (since it might have new actors) | ||
// - OR the incoming state actually changed. | ||
// | ||
// The "live" initial state will have .changed === undefined. | ||
var initialStateChanged = currentState.changed === undefined && | ||
Object.keys(currentState.children).length; | ||
if (currentState.changed || initialStateChanged) { | ||
setState(currentState); | ||
} | ||
if (currentState.actions.length) { | ||
var reactEffectActions = currentState.actions.filter(function (action) { | ||
return (typeof action.exec === 'function' && | ||
'__effect' in | ||
action.exec); | ||
}); | ||
var _c = __read(utils_1.partition(reactEffectActions, function (action) { | ||
return action.exec.__effect === ReactEffectType.Effect; | ||
}), 2), effectActions = _c[0], layoutEffectActions = _c[1]; | ||
(_a = effectActionsRef.current).push.apply(_a, __spread(effectActions.map(function (effectAction) { return [effectAction, currentState]; }))); | ||
(_b = layoutEffectActionsRef.current).push.apply(_b, __spread(layoutEffectActions.map(function (layoutEffectAction) { return [layoutEffectAction, currentState]; }))); | ||
} | ||
}) | ||
.start(rehydratedState ? xstate_1.State.create(rehydratedState) : undefined); | ||
return function () { | ||
service.stop(); | ||
}; | ||
}, []); | ||
// Make sure actions and services are kept updated when they change. | ||
@@ -87,25 +165,16 @@ // This mutation assignment is safe because the service instance is only used | ||
}, [services]); | ||
// this is somewhat weird - this should always be flushed within useLayoutEffect | ||
// but we don't want to receive warnings about useLayoutEffect being used on the server | ||
// so we have to use `useIsomorphicLayoutEffect` to silence those warnings | ||
use_isomorphic_layout_effect_1.default(function () { | ||
while (layoutEffectActionsRef.current.length) { | ||
var _a = __read(layoutEffectActionsRef.current.shift(), 2), layoutEffectAction = _a[0], effectState = _a[1]; | ||
executeEffect(layoutEffectAction, effectState); | ||
} | ||
}, [state]); // https://github.com/davidkpiano/xstate/pull/1202#discussion_r429677773 | ||
react_1.useEffect(function () { | ||
// Whenever a new state is emitted from the service, | ||
// update the state with that state, but only if | ||
// that state has changed. | ||
service.onTransition(function (currentState) { | ||
if (currentState.changed) { | ||
// capture side-effects to be executed | ||
actionStatesRef.current.push(currentState); | ||
// change state | ||
setState(currentState); | ||
} | ||
}); | ||
return function () { | ||
service.stop(); | ||
}; | ||
}, []); | ||
react_1.useEffect(function () { | ||
// Flush all actions to be executed (per state) | ||
actionStatesRef.current.forEach(function (actionState) { | ||
// Execute all actions for the captured state | ||
service.execute(actionState); | ||
}); | ||
actionStatesRef.current = []; | ||
while (effectActionsRef.current.length) { | ||
var _a = __read(effectActionsRef.current.shift(), 2), effectAction = _a[0], effectState = _a[1]; | ||
executeEffect(effectAction, effectState); | ||
} | ||
}, [state]); | ||
@@ -112,0 +181,0 @@ return [state, service.send, service]; |
@@ -1,3 +0,5 @@ | ||
import { EventObject, State, Interpreter, Typestate, Event } from 'xstate'; | ||
export declare function useService<TContext, TEvent extends EventObject, TTypestate extends Typestate<TContext> = any>(service: Interpreter<TContext, any, TEvent, TTypestate>): [State<TContext, TEvent, any, TTypestate>, (event: Event<TEvent>) => void]; | ||
import { EventObject, State, Interpreter, Typestate, Sender } from 'xstate'; | ||
import { ActorRef } from './types'; | ||
export declare function fromService<TContext, TEvent extends EventObject>(service: Interpreter<TContext, any, TEvent, any>): ActorRef<TEvent, State<TContext, TEvent>>; | ||
export declare function useService<TContext, TEvent extends EventObject, TTypestate extends Typestate<TContext> = any>(service: Interpreter<TContext, any, TEvent, TTypestate>): [State<TContext, TEvent, any, TTypestate>, Sender<TEvent>]; | ||
//# sourceMappingURL=useService.d.ts.map |
"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 }); | ||
var react_1 = require("react"); | ||
var useActor_1 = require("./useActor"); | ||
var ActorRef_1 = require("./ActorRef"); | ||
function fromService(service) { | ||
var machine = service.machine; | ||
return { | ||
send: service.send.bind(service), | ||
subscribe: service.subscribe.bind(service), | ||
stop: service.stop, | ||
current: service.initialized ? service.state : machine.initialState, | ||
name: service.sessionId | ||
}; | ||
} | ||
exports.fromService = fromService; | ||
function useService(service) { | ||
var actorRef = react_1.useMemo(function () { | ||
return ActorRef_1.fromService(service); | ||
}, [service]); | ||
var _a = __read(useActor_1.useActor(actorRef), 2), state = _a[0], sendActor = _a[1]; | ||
var send = react_1.useCallback(function (event) { | ||
var eventObject = typeof event === 'string' ? { type: event } : event; | ||
sendActor(eventObject); | ||
}, [sendActor]); | ||
return [state, send]; | ||
var serviceActor = react_1.useMemo(function () { return fromService(service); }, [service]); | ||
return useActor_1.useActor(serviceActor); | ||
} | ||
exports.useService = useService; |
{ | ||
"name": "@xstate/react", | ||
"version": "1.0.0-rc.4", | ||
"version": "1.0.0-rc.5", | ||
"description": "XState tools for React", | ||
@@ -37,4 +37,3 @@ "keywords": [ | ||
"test": "jest", | ||
"prepublish": "npm run build && npm run test", | ||
"prerelease": "npm publish --tag next" | ||
"prepublish": "npm run build && npm run test" | ||
}, | ||
@@ -58,3 +57,4 @@ "bugs": { | ||
"dependencies": { | ||
"use-subscription": "^1.4.0" | ||
"use-isomorphic-layout-effect": "^1.0.0", | ||
"use-subscription": "^1.3.0" | ||
}, | ||
@@ -61,0 +61,0 @@ "devDependencies": { |
@@ -73,4 +73,50 @@ # @xstate/react | ||
### `useMachine(machine)` with `@xstate/fsm` <Badge text="1.1+"/> | ||
### `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. | ||
@@ -202,6 +248,7 @@ | ||
actions: { | ||
notifySuccess: ctx => onResolve(ctx.data) | ||
notifySuccess: (ctx) => onResolve(ctx.data) | ||
}, | ||
services: { | ||
fetchData: (_, e) => fetch(`some/api/${e.query}`).then(res => res.json()) | ||
fetchData: (_, e) => | ||
fetch(`some/api/${e.query}`).then((res) => res.json()) | ||
} | ||
@@ -257,7 +304,7 @@ }); | ||
switch (true) { | ||
case current.matches('idle'): | ||
case state.matches('idle'): | ||
return /* ... */; | ||
case current.matches({ loading: 'user' }): | ||
case state.matches({ loading: 'user' }): | ||
return /* ... */; | ||
case current.matches({ loading: 'friends' }): | ||
case state.matches({ loading: 'friends' }): | ||
return /* ... */; | ||
@@ -325,3 +372,3 @@ default: | ||
useEffect(() => { | ||
const subscription = service.subscribe(state => { | ||
const subscription = service.subscribe((state) => { | ||
// simple state logging | ||
@@ -328,0 +375,0 @@ console.log(state); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
34513
19
497
381
5
+ Addeduse-isomorphic-layout-effect@1.2.0(transitive)
Updateduse-subscription@^1.3.0