Question
The interpretation page in the docs says that it is the interpreter's job to "Keep track of the current state, and persist it" yet xstate throws an error if machine.transition is called with an object that has been serialized and deserialized using JSON.
What is the correct way to serialize a state object?
Using version 3.3.3
xstate returns the new state.
xstate throws:
Error: Child state 'value' does not exist on 'myStateMachine'
If this is a bug (though I suspect it is a misunderstanding on my part) I'll try to find a fix.
'use strict'
const util = require('util')
const xstate = require('xstate')
const machine = xstate.Machine({
key: 'myStateMachine',
initial: 'firstState',
states: {
firstState: {
on: { transitionToSecond: 'secondState' },
},
secondState: {
on: { transitionToThird: 'thirdState' },
},
thirdState: {},
},
})
try {
let state
state = machine.transition(machine.initialState, 'transitionToSecond')
console.log('state after transitionToSecond:\n\n'
+ util.inspect(state, {showHidden: true, depth: null, breakLength: 1})
+ '\n')
state = JSON.parse(JSON.stringify(state))
// Error: Child state 'value' does not exist on 'myStateMachine'
state = machine.transition(state, 'transitionToThird')
console.log('state after transitionToSecond:\n\n'
+ util.inspect(state, {showHidden: true, depth: null, breakLength: 1})
+ '\n')
} catch (error) {
console.log(error)
}
When you pass in an object, xstate considers that object the current state value. Only when you pass it an instance of a State does it look for the current state value in state.value. This is as intended.
However, as I'm working on v4, do you think we should add a __type: "state" key or some other special key to signify that this is a State instance (and duck-type it instead)?
Would love to hear thoughts.
Rather than a design change like that, simply implementing toJSON to only return the stringified value property would have prevented my amature mistake.
However, now I see that I am losing my historyValue, and the docs on the State constructor only mention the history argument. Is there anything I can do to make the example below pass the assertion?
'use strict'
const util = require('util')
const assert = require('assert')
const xstate = require('xstate')
const machine = xstate.Machine({
key: 'myStateMachine',
initial: 'firstState',
states: {
firstState: {
on: { transitionToSecond: 'secondState.hist' },
},
secondState: {
initial: 'hist',
states: {
hist: {
history: true,
target: 'aSubstate',
},
aSubstate: {
on: { transitionToB: 'bSubstate' }
},
bSubstate: {
on: { transitionToThird: '#myStateMachine.thirdState' }
},
},
},
thirdState: {
on: { transitionToSecond: 'secondState.hist' },
}
},
})
try {
let state
console.log('initialState:\n\n'
+ util.inspect(machine.initialState, {showHidden: true, depth: null, breakLength: 1})
+ '\n')
state = JSON.parse(JSON.stringify(machine.initialState.value))
state = machine.transition(state, 'transitionToSecond')
console.log('state after transitionToSecond:\n\n'
+ util.inspect(state, {showHidden: true, depth: null, breakLength: 1})
+ '\n')
state = JSON.parse(JSON.stringify(state.value))
state = machine.transition(state, 'transitionToB')
console.log('state after transitionToB:\n\n'
+ util.inspect(state, {showHidden: true, depth: null, breakLength: 1})
+ '\n')
state = JSON.parse(JSON.stringify(state.value))
state = machine.transition(state, 'transitionToThird')
console.log('state after transitionToThird:\n\n'
+ util.inspect(state, {showHidden: true, depth: null, breakLength: 1})
+ '\n')
state = JSON.parse(JSON.stringify(state.value))
state = machine.transition(state, 'transitionToSecond')
console.log('state after transitionToSecond:\n\n'
+ util.inspect(state, {showHidden: true, depth: null, breakLength: 1})
+ '\n')
// AssertionError [ERR_ASSERTION]: history failed to restore.
assert(state.value.secondState === 'bSubstate', 'history failed to restore.')
} catch (error) {
console.log(error)
}
Right now (v3.3) can manually create a State instance and pass it in prop-by-prop, which looks like this:
const state = new State(
value: StateValue,
context: TContext,
historyValue?: HistoryValue | undefined,
history?: State<TContext>,
actions: Array<ActionObject<TContext>> = [],
activities: ActivityMap = EMPTY_ACTIVITY_MAP,
data: Record<string, any> = {}
);
However, I see that this is a valid use case (state serialization) so it might be better to create a new State.fromDefinition() static method, so here's a potential V4 API:
const json = // ... JSON string
const stateDefinition = JSON.parse(json);
// TENTATIVE API
// This will do two things:
// 1. verify that it fits a "state definition" (value, history, actions, etc.)
// 2. create a new State instance based on this raw object
const restoredState = State.fromDefinition(stateDefinition);
In v4, you will be able to start the interpreter at that specific state:
const interpreter = interpret(myMachine);
// Start at the restored state instead of the initial state
// (This is already in master)
interpreter.start(restoredState);
Thanks for the guidance, I've been able to get my app working. A static State.fromDefinition() would fit my use case perfectly. It sounds like there are a lot of great features coming, I'll be watching the progress and waiting for the release of V4!
This is now State.create(config), where config is a deserialized JSON object.
Most helpful comment
Right now (v3.3) can manually create a
Stateinstance and pass it in prop-by-prop, which looks like this:However, I see that this is a valid use case (state serialization) so it might be better to create a new
State.fromDefinition()static method, so here's a potential V4 API:In v4, you will be able to start the interpreter at that specific state: