XState (2)

States

state.hasTag

state에 tag를 관리하게 할 수 있다. 아래의 코드와 같이 greenyellow가 동일한 상태로서 매칭된다면 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 부터 이어서 진행 예정입니다.

19