idle
pending
resolved
rejected
fetch
resolve
reject
resolved
Using switch/case
idle
pending
resolved
rejected
fetch
resolve
reject
resolved
function transition(state, event) {
  switch (state) {
    case 'idle':
      switch (event) {
        case 'FETCH':
          return 'pending';
        default:
          return state;
      }
    case 'pending':
      switch (event) {
        case 'RESOLVE':
          return 'resolved';
        case 'REJECT':
          return 'rejected';
        default:
          return state;
      }
    default:
      return state;
  }
}
Using objects
idle
pending
resolved
rejected
fetch
resolve
reject
resolved
import { Machine } from 'xstate';
const fetchMachine = Machine({
  id: 'fetch',
  initial: 'idle',
  states: {
    idle: {},
    pending: {},
    resolved: {},
    rejected: {}
  }
});Configuring the finite states
Using objects
idle
pending
resolved
rejected
fetch
resolve
reject
resolved
import { Machine } from 'xstate';
const fetchMachine = Machine({
  id: 'fetch',
  initial: 'idle',
  states: {
    idle: {
      on: {
        FETCH: 'pending'
      }
    },
    pending: {
      on: {
        RESOLVE: 'resolved',
        REJECT: 'rejected'
      }
    },
    resolved: {
      type: 'final'
    },
    rejected: {}
  }
});Configuring the transitions
idle
pending
resolved
rejected
fetch
resolve
reject
resolved
const nextState = fetchMachine
  .transition('idle', 'FETCH');
// Finite state value
nextState.value;
// => 'pending'
// Extended state (context)
nextState.context;
// => undefined
// Actions
nextState.actions;
// => []import { Machine, interpret } from 'xstate';
const fetchMachine = Machine({/* ... */});
const fetchService = interpret(fetchMachine);
// Add state change listener
fetchService.onTransition(state => {
  console.log(state);
});
// Start the service
fetchService.start();
// Send events
fetchService.send('FETCH');
fetchService.send({
  type: 'RESOLVE',
  items: [/* ... */]
});
// Stop the service
fetchService.stop();Feedback App
import { Machine, interpret } from 'xstate';
const feedbackMachine = Machine({
  id: 'feedback',
  initial: 'question',
  states: {
    question: {},
    form: {},
    thanks: {},
    closed: {}
  }
});import { Machine, interpret } from 'xstate';
const feedbackMachine = Machine({
  id: 'feedback',
  initial: 'question',
  states: {
    question: {
      on: {
        GOOD: 'thanks',
        BAD: 'form',
        CLOSE: 'closed',
        ESC: 'closed'
      }
    },
    form: {
      on: {
        SUBMIT: 'thanks'
        CLOSE: 'closed',
        ESC: 'closed'
      }
    },
    thanks: {
      on: {
        CLOSE: 'closed',
        ESC: 'closed'
      }
    },
    closed: {}
  }
});
const feedbackService = interpret(feedbackMachine)
  .onTransition(state => {
    console.log(state);
  })
  .start();npm install xstate --save
npm install @xstate/react --save
Install the XState core library
Install the XState React hook
useMachine hook
import React from 'react';
import { useMachine } from '@xstate/react';
// Existing machine
import { feedbackMachine } from './feedbackMachine';
export const App = () => {
  // Returns tuple of:
  // 1. current state
  // 2. send function
  const [current, send] =
    useMachine(feedbackMachine);
  return (
    /* ... */
  );
};import React from 'react';
import { useMachine } from '@xstate/react';
// Existing machine
import { feedbackMachine } from './feedbackMachine';
export const App = () => {
  // Returns tuple of:
  // 1. current state
  // 2. send function
  const [current, send] = useMachine(feedbackMachine);
  console.log(current);
  return (
    /* ... */
  );
};state.matches(...)
export const App = () => {
  const [current, send] = useMachine(feedbackMachine);
  return (
    <section>
      {current.matches('question') ? (
        'How was your experience?'
      ) : current.matches('form') ? (
        'Form here'
      ) : current.matches('acknowledge') ? (
        'Thanks for your feedback!'
      ) : null}
    </section>
  );
};send(...)
// Event without payload
<button
  onClick={() => send('CLICK_GOOD')}
>
// Event with payload
<form
  onSubmit={e => {
    // ...
    send({
      type: 'SUBMIT',
      value: // ...
    });
  }}
>onEntry and onExit
// ...
form: {
  onEntry: (ctx, e) => {
    // code here to focus the input
  },
  on: {
    SUBMIT: 'thanks'
    CLOSE: 'closed',
    ESC: 'closed'
  },
  onExit: (ctx, e) => {
    // code here to log "exited"
  }
},
// ...// ...
form: {
  onEntry: 'focusInput',
  on: {
    SUBMIT: 'thanks'
    CLOSE: 'closed',
    ESC: 'closed'
  },
  onExit: 'logExited'
},
// ...// ...
form: {
  onEntry: ['logEntered', 'focusInput'],
  on: {
    SUBMIT: 'thanks'
    CLOSE: 'closed',
    ESC: 'closed'
  },
  onExit: 'logExited'
},
// ...feedbackService.send('BAD');
// State {
//   value: 'form',
//   context: undefined,
//   actions: [
//     { type: 'logEntered' },
//     { type: 'focusInput' }
//   ],
//   ...
// }Adding machine options
const feedbackMachine = Machine({
  id: 'feedback',
  // ...
}, {
  actions: {
    focusInput: (ctx, e) => {
      // code to focus input
    }
  }
});Extending existing machines
const myMachine = feedbackMachine
  .withConfig({
    actions: {
      focusInput: (ctx, e) => {
        // code to focus input
      }
    }
  });AKA Extended State
const feedbackMachine = Machine({
  id: 'feedback',
  context: {
    response: ''
  },
  initial: 'question',
  states: {
    // ...
  }
});feedbackService.initialState
  .value; // => 'question'
feedbackService.initialState
  .context; // { response: '' }assign(...)
import { Machine, assign } from 'xstate';
// ...
form: {
  on: {
    CHANGE: {
      // no target!
      actions: assign({
        response: (ctx, e) => e.value
      })
    }
  }
}
//...state.context
const nextState = feedbackMachine
  .transition('form', {
    type: 'CHANGE',
    value: 'some response'
  });
nextState.context;
// => { response: 'some response' }<textarea
  onChange={e => {
    send({
      type: 'CHANGE',
      value: e.target.value
    });
  }}
  value={current.context.response}
/>cond: (ctx, e) => Boolean
// ...
on: {
  SUBMIT: [
    {
      target: 'thanks',
      cond: (ctx, e) => ctx.response.length > 0
    },
    { target: 'form' }
  ]
}
//...// ...
on: {
  SUBMIT: [
    {
      target: 'thanks',
      cond: 'formValid'
    },
    { target: 'form' }
  ]
}
//...const myMachine = feedbackMachine
  .withConfig({
    actions: {
      // ...
    },
    guards: {
      formValid: (ctx, e) => {
        return ctx.response.length > 0;
      }
    }
  });AKA Hierarchical States
// ...
form: {
  initial: 'pending',
  states: {
    pending: {
      on: {
        SUBMIT: [
          { target: 'submitted', cond: 'formValid' },
          { target: 'invalid' }
        ]
      }
    },
    invalid: {
      on: {
        FOCUS: 'pending'
      }
    },
    submitted: {}
  },
  on: {
    CLOSE: 'closed',
    ESC: 'closed'
  }
},
// ...State Values
const pendingState = feedbackMachine
  .transition('question', 'BAD')
  .value;
// => { form: 'pending' }
pendingState.matches('form');
// => true
pendingState.matches('form.pending');
// => true
pendingState.matches({ form: 'pending' });
// => true// ...
form: {
  initial: 'pending',
  states: {
    pending: {/* ... */},
    invalid: {/* ... */},
    submitted: {
      type: 'final'
    }
  },
  onDone: 'thanks'
}
// ...Invoking a Promise
// ...
form: {
  initial: 'pending',
  states: {/* ... */},
  onDone: 'sending'
},
sending: {
  invoke: {
    src: (ctx, e) => {
      // returns a Promise
      return sendFeedback(ctx.response);
    },
    onDone: 'thanks'
  }
}
// ...Serializing Invoke Sources
// ...
form: {
  initial: 'pending',
  states: {/* ... */},
  onDone: 'sending'
},
sending: {
  invoke: {
    src: 'sendFeedback',
    onDone: 'thanks'
  }
}
// ...Configuring Invoke Sources
const feedbackMachine = Machine({
  id: 'feedback',
  // ...
}, {
  actions: {/* ... */},
  guards: {/* ... */},
  services: {
    sendFeedback: (ctx, e) => {
      return sendFeedback(ctx.response);
    }
  }
});const myMachine = feedbackMachine
  .withConfig({
    actions: {/* ... */},
    guards: {/* ... */},
    services: {
      sendFeedback: (ctx, e) => {
        return sendFeedback(ctx.response);
      }
    }
  });Generating Simple Paths
import { getSimplePaths } from '@xstate/graph';
const simplePaths = getSimplePaths(
  feedbackMachine, {
    events: {
      // Provide some sample events
      SUBMIT: [
        {
          type: 'SUBMIT',
          value: 'test feedback input'
        }
      ]
    }
  });Simple Paths
{
  "\"question\"": {
    "state": {
      "value": "question"
    },
    "paths": [[]]
  },
  "\"thanks\"": {
    "state": {
      "value": "thanks"
    },
    "paths": [
      [
        {
          "state": {
            "value": "question"
          },
          "event": {
            "type": "CLICK_GOOD"
          }
        }
      ],
      [
        {
          "state": {
            "value": "question"
          },
          "event": {
            "type": "CLICK_BAD"
          }
        },
        {
          "state": {
            "value": "form"
          },
          "event": {
            "type": "SUBMIT",
            "value": "test feedback input"
          }
        }
      ]
    ]
  },
  ...
}Using react-testing-library
describe('feedback app', () => {
  Object.keys(simplePaths).forEach(key => {
    const { paths, state: targetState } = simplePaths[key];
    describe(`state: ${key}`, () => {
      afterEach(cleanup);
      paths.forEach(path => {
        const eventString = path.length
          ? 'via ' + path.map(step => step.event.type).join(', ')
          : '';
        it(`reaches ${key} ${eventString}`, async () => {
          // Render the feedback app
          
          // Add heuristics for asserting that the state is correct
          
          // Add actions that will be executed (and asserted) to produce the events
          
          // Loop through each of the steps, assert the state, assert/execute the action
          
          // Finally, assert that the target state is reached.
        });
      });
    });
  });
});Rendering the app
import Feedback from './Feedback';
import { render, fireEvent, cleanup } from 'react-testing-library';
// ...
  // Render the feedback app
  const {
    getByTestId,
    baseElement,
    queryByTestId
  } = render(<Feedback />);
// ...Asserting State
// ... 
import { Machine, matchesState } from 'xstate';
// ...
// Add heuristics for asserting that the state is correct
function assertState(state) {
  if (state.matches('question')) {
    // assert that the question screen is visible
    assert.ok(getByTestId('question-screen'));
  } else if (state.matches('form')) {
    // assert that the form screen is visible
    assert.ok(getByTestId('form-screen'));
  } else if (state.matches('acknowledge')) {
    // assert that the acknowledge screen is visible
    assert.ok(getByTestId('acknowledge-screen'));
  } else if (state.matches('closed')) {
    // assert that the acknowledge screen is hidden
    assert.isNull(queryByTestId('acknowledge-screen'));
  }
}
// ...Executing Actions
// Add actions that will be executed (and asserted) to produce the events
function assertAction(event) {
  const action = {
    CLICK_GOOD: () => {
      fireEvent.click(getByTestId('good-button'));
    },
    CLICK_BAD: () => {
      fireEvent.click(getByTestId('bad-button'));
    },
    SUBMIT: e => {
      fireEvent.change(getByTestId('response-input'), {
        target: { value: e.value }
      });
      fireEvent.click(getByTestId('submit-button'));
    },
    CLOSE: () => {
      fireEvent.click(getByTestId('close-button'));
    },
    ESC: () => {
      fireEvent.keyDown(baseElement, { key: 'Escape' });
    }
  }[event.type];
  if (action) {
    // Execute the action
    action(event);
  } else {
    throw new Error(`Action for event '${event.type}' not found`);
  }
}Testing Each Path
// Loop through each of the steps,
// assert the state,
// assert/execute the action
for (let step of path) {
  const { state, event } = step;
  await assertState(state);
  await assertAction(event);
}
// Finally, assert that the
// target state is reached.
await assertState(targetState);Running the Tests
 PASS  src/App.test.js
  feedback app
    state: "question"
      ✓ reaches "question"  (9ms)
    state: "thanks"
      ✓ reaches "thanks" via CLICK_GOOD (7ms)
      ✓ reaches "thanks" via CLICK_BAD, SUBMIT (9ms)
    state: "closed"
      ✓ reaches "closed" via CLICK_GOOD, CLOSE (6ms)
      ✓ reaches "closed" via CLICK_GOOD, ESC (7ms)
      ✓ reaches "closed" via CLICK_BAD, SUBMIT, CLOSE (10ms)
      ✓ reaches "closed" via CLICK_BAD, SUBMIT, ESC (13ms)
      ✓ reaches "closed" via CLICK_BAD, CLOSE (7ms)
      ✓ reaches "closed" via CLICK_BAD, ESC (5ms)
      ✓ reaches "closed" via CLOSE (3ms)
      ✓ reaches "closed" via ESC (3ms)
    state: "form"
      ✓ reaches "form" via CLICK_BAD (5ms)
Test Suites: 1 passed, 1 total
Tests:       12 passed, 12 total
Snapshots:   0 total
Time:        0.41s, estimated 1sWriting stories
Workshop Complete 🎉