19
XState (2)
state에 tag를 관리하게 할 수 있다. 아래의 코드와 같이 green
과 yellow
가 동일한 상태로서 매칭된다면 state.matches('green') || state.matchs('yellow')
대신 사용할 수 있다.
const machine = createMachine({
initial: 'green',
states: {
green: {
tags: 'go' // single tag
},
yellow: {
tags: 'go'
},
red: {
tags: ['stop', 'other'] // multiple tags
}
}
});
const canGo = state.hasTag('go');
// === state.matches('green') || state.matchs('yellow')
State
객체는 string json format으로 직렬화하여 localstorage 등의 값으로 부터 초기화 될 수 있다.
const jsonState = JSON.stringify(currentState);
// 어딘가에서 저장하고..
try {
localStorage.setItem('app-state', jsonState);
} catch (e) {
// unable to save to localStorage
}
// ...
const stateDefinition =
JSON.parse(localStorage.getItem('app-state')) || myMachine.initialState;
// State.create()를 이용하여 plain object로 부터 스토어를 복구시킴
const previousState = State.create(stateDefinition);
// machine.resolveState()를 이용하여 새 상태로 정의됨
const resolvedState = myMachine.resolveState(previousState);
React와 사용할땐 어떻게 사용할까 하고 보니 @xstate/react
에서는 아래와 같이 간단하게 쓰고 있다.
// ...
// 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 (/* ... */)
}
state node의 관련 속성을 설명하는 정적 데이터인 메타 데이터는 meta
속성에 지정할 수 있다.
const lightMachine = createMachine({
id: 'light',
initial: 'green',
states: {
green: {
tags: 'go',
meta: {
message: 'can go',
},
on: { 'WILL_STOP': 'yellow' },
},
yellow: {
tags: 'go',
meta: {
message: 'will be red',
},
on: { 'STOP': 'red' }
},
red: {
tags: ['stop', 'other'],
meta: {
message: 'stop!',
},
on: { 'GO': 'green' }
}
}
});
const yellowState = lightMachine.transition('green', {
type: 'WILL_STOP'
});
console.log(yellowState.meta);
// { 'light.yellow': { message: 'will be red' } }
(공식문서 보고 {'yellow': { message: 'will be red' }}
를 기대했었는데..)
meta를 여러개 포함할때도 모두 표현해준다.
const fetchMachine = createMachine({
id: 'fetch',
// ... 중략
loading: {
after: {
3000: 'failure.timeout'
},
on: {
RESOLVE: { target: 'success' },
REJECT: { target: 'failure' },
TIMEOUT: { target: 'failure.timeout' } // manual timeout
},
meta: {
message: 'Loading...'
}
},
failure: {
initial: 'rejection',
states: {
rejection: {
meta: {
message: 'The request failed.'
}
},
timeout: {
meta: {
message: 'The request timed out.'
}
}
},
meta: {
alert: 'Uh oh.'
}
},
// ... 하략
});
const failureTimeoutState = fetchMachine.transition('loading', {
type: 'TIMEOUT'
});
console.log(fetchMachine.meta)
/*
{
"fetch.failure.timeout': {
'message': 'The request timed out.',
},
'fetch.failure': {
'alert": "Uh oh.',
}
}
*/
state machine은 전체 상태(overall state)를 집합으로 표현되는 상태 노드(State Node)를 포함한다. 아래의 상태 명세는 위 예제에서 가져왔다.
// 위의 fetchMachine의 loading 참고
// 해당 State의 configuration 내부의 config에서 확인 가능.
{
'after': {
'3000': 'failure.timeout'
},
'on': {
'RESOLVE': {
'target': 'success'
},
'REJECT': {
'target': 'failure'
},
'TIMEOUT': {
'target': 'failure.timeout'
}
},
'meta': {
'message': 'Loading...'
}
}
전체 State는 machine.transition()
의 리턴 값이나 service.onTransition()
의 콜백 값에서도 있다.
const nextState = fetchMachine.transition('idle', { type: 'FETCH' });
// State {
// value: 'loading',
// actions: [],
// context: undefined,
// configuration: [ ... ]
// ...
// }
XState에서 상태 노드는 state configuration으로 지정된다. 이들은 machines의 states
property에 정의되어있다. 하위 상태(sub-state) 노드 역시 마찬가지로 계층 구조로서 states
property의 상태 노드에 선언될 수 있다. 'machine.transition(state, event)'에서 결정된 상태는 상태 노드의 조합을 나타낸다. 예를들어 아래의 success
와 하위 상태 items
는 { success: 'items' }
로 표현된다.
const fetchMachine = createMachine({
id: 'fetch',
// 이것도 States 이고
states: {
success: {
// 자식 상태를 초기화 하고
initial: { target: 'items' },
// 자식 상태임.
states: {
items: {
on: {
'ITEM.CLICK': { target: 'item' }
}
},
item: {
on: {
BACK: { target: 'items' }
}
}
}
},
});
상태 노드는 5가지가 있다.
-
atomic
- 자식 상태가 없는 노드 (leaf node) -
compound
- 하나 이상의 상태를 포함하며 이런 하위 상태 중 하나가 키인intial
상태가 있다. -
parallel
- 2개 이상의 하위 상태를 포함하며 동시에 모든 하위 상태가 있다는 것을 나타내기 위함이어서 초기상태가 없다. (약간의 의역인데 이렇게 이해했음..) -
final
- 추상적으로 "말단" 상태임을 나타내는 단말 노드이다. -
history
- 부모 노드의 가장 최근의 shallow or deep history 상태를 나타내는 추상 노드
아래 선언부를 보니까 조금 더 이해가 되었다.
const machine = createMachine({
id: 'fetch',
initial: 'idle',
states: {
idle: {
// 단일 노드
type: 'atomic',
on: {
FETCH: { target: 'pending' }
}
},
pending: {
// resource1, resource2 두개가 parallel 하게 있구나..
type: 'parallel',
states: {
resource1: {
// 내부에 pending, success 두가지를 갖는 복합 상태
type: 'compound',
initial: 'pending',
states: {
pending: {
on: {
'FULFILL.resource1': { target: 'success' }
}
},
success: {
type: 'final'
}
}
},
resource2: {
type: 'compound',
initial: 'pending',
states: {
pending: {
on: {
'FULFILL.resource2': { target: 'success' }
}
},
success: {
type: 'final'
}
}
}
},
// resource1, resource2 둘 다 final 상태가 되면 success로
onDone: 'success'
},
success: {
type: 'compound',
initial: 'items',
states: {
items: {
on: {
'ITEM.CLICK': { target: 'item' }
}
},
item: {
on: {
BACK: { target: 'items' }
}
},
hist: {
type: 'history',
history: 'shallow'
}
}
}
}
});
유형을 명시적으로 지정하면 typescript 분석 및 유형검사 관련으로 유용하다고 하는데, parallel
, history
, final
만 해당된다.
이후 3편에서 Transient State Nodes
부터 이어서 진행 예정입니다.
19