XState (2)
Park Je Hoon
Posted on June 23, 2021
1편에 이어서..
States
state.hasTag
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')
Persisting State
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 Meta Data
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 Node
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
부터 이어서 진행 예정입니다.
Posted on June 23, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.