Vue 3, XState, and CSS Starter Kit
This starter kit uses Vue 3, XState for state management, and pure CSS for styling.
How to use this starter kit
npm create @this-dot/starter -- --kit vue3-xstate-css
(use any of npm
/ yarn
/ pnpm
)
For more setup options, check out our setup instructions in the wiki.
Tech Stack
This kit is also set up to show the XState visualizer when run locally, to help you see what your state machines look like and how they work.
Included Tooling
- Vite - build / bundle tool
- Vue Router - navigation
- Cypress - component / unit testing
- Storybook - documents component designs
- ESLint - code consistency and best practices
- Prettier - code formatting
Type Support for .vue
Imports in TS
This project is setup to handle TypeScript files. However, TypeScript cannot handle type information for .vue
imports by default, so we replace the tsc
CLI with vue-tsc
for type checking. In editors, we need TypeScript Vue Plugin (Volar) to make the TypeScript language service aware of .vue
types.
Getting Started
Prerequisites
- Node.js 16.8 or later installed
Development
Once you’ve generated your new starter kit following the instructions above:
cd
into your project directory and runnpm install
to install the dependencies.- Run
npm run dev
to start the development server. - Click the http://localhost:5173 link from within your terminal. This will open that link in a tab to view the application, and also launch the XState inspector for you in a second tab.
Available commands
-
npm run dev
starts the local development server -
npm run build-only
handles compiling and minifying your files -
npm run type-check
type checks your files -
npm run build
combines the previous two commands to type-check your files then compile and minify them for production -
npm run preview
will run the site locally based on your production built files generated by the build commands -
npm run test:unit
runs Cypress component tests in a headless state, as they would run in a CI environment -
npm run test:unit:dev
runs Cypress component tests in a headed state, so you can walk through individual tests -
npm run lint
checks your files for common coding errors -
npm run format
formats all of your files -
npm run storybook
runs Storybook locally -
npm run build-storybook
builds a version of your Storybook stories that you can host somewhere
Project Details
Project Structure
The main folder you’ll interact with is the src
folder. This is split into a few different folders to help keep concerns organized.
assets
: for global CSS or images that will need to be bundled with your codecomponents
: small, reusable pieces of code that don’t need any specific store logicmachines
: to hold our state machinesrouter
: routing configurationstories
: Storybook storiestests
: Cypress component testsutils
: any reusable helper functions you might need to createviews
: the actual “views” or pages you assemble, which will combine pieces from all the previous folders
Using XState
The visualizer
To get the visualizer working locally, we’re using the @xstate/inspect
package and importing it within the main.ts
file. Then within our components, when we want to be able to visualize our state machine we pass the devtools: true
option when we call useMachine
. The visualizer allows us to see what a particular state machine looks like by letting us manually trigger different states and actions. You can even directly change the machine within the visualizer to test out changes you might want to make. Just make sure that if you want to keep those changes you copy it back into your machine file!
State vs Context
You’ll notice that within both of our example machines, we’re using a context
object. You can read more specifics about how this works in the XState documentation on context, but in general the context stores data that might be quantitative in nature (like numbers, strings, or objects).
In the introduction to state machines documentation, they give the example of a dog’s “state” as being asleep or awake. A dog can’t be both asleep and awake at the same time - it has to be one or the other. The same is true of our states. You’ll see this best in the greetMachine.ts
file. We’re either loading our data, we received our complete data, or we received our error data. The machine can only be in one of these three states.
We use the context to store information that might change as we go through different states. In our greetMachine, we use the context to store the query we’re sending, the message we get back, and any potential error text we get back. Since the items here are arbitrary and can change as our machine moves through it’s states, we store these in the context object.
TS Support in Machines
XState offers us a schema
option in our machines to allow us to type our state charts. We can use this to help strongly type our context and events. This will give us better tips when using our machine and help ensure we know what types of values we’re expecting when we use them.
Machine Configuration
The createMachine
call that we use to build machines accepts two objects.
The first object is always present, and specifies the name of our machine, it’s initial state, any local context it needs, and the states our machine can be in.
import { createMachine } from 'xstate';
const lightMachine = createMachine({
// Machine identifier
id: 'light',
// Initial state
initial: 'green',
// Local context for entire machine
context: {
elapsed: 0,
direction: 'east',
},
// State definitions
states: {
green: {
/* ... */
},
yellow: {
/* ... */
},
red: {
/* ... */
},
},
});
Eventually our states will need to define actions, services, or guards. These can be written directly within the state itself, or they can be passed to the optional second object in the createMachine
function, and then referenced in the state by their name.
const lightMachine = createMachine(
{
id: 'light',
initial: 'green',
states: {
green: {
// action referenced via string
entry: 'alertGreen',
},
},
},
{
actions: {
// action implementation
alertGreen: (context, event) => {
alert('Green!');
},
},
guards: {
/* ... */
},
services: {
/* ... */
},
}
);
You’ll see both options in this kit - the counterMachine
has it’s actions defined separately, and the greetMachine
has the actions inline in the state but the service defined separately. Either option is valid. You might start with your actions inlined to ensure they work, and then separate them out to make them easier to read and debug.
Predictable Action Arguments Flag
You’ll see the option predictableActionArguments: true
within our machines. This is recommended per the docs, and will be a default option in the next version. This flag means that XState will always call an action with the direct event that triggered it.
Vue 3 Benefits
Since we’re using Vue 3, we’re able to make use of the composition API. This means we can use the setup
option in our component’s script tags, which lets us define our variables and functions in a style that looks a bit more like standard JS. With this method, we don’t have to have an object that defines all of the values our component can use - it allows us to use standard variables and functions which can be used directly in our templates.
Another benefit of using Vue 3 is getting to work with the new provide and inject functions. These allow us to set up our own dependency system, so instead of having to deal with prop drilling and passing values along components that don’t need them, we can provide that value in a parent component and then inject it into the component that needs it, bypassing all the others.
You can see an example of this with the GreetView
component. We needed a way to provide an initial query value, but didn’t want to have to set up a prop within the router or drill it down through the home component. So we set up the provide
function in the main.ts
file, which makes it globally available in our app to any component that needs it. The provide
function takes two arguments, a key and a value. Then, our GreetView
component can inject that value and make use of it. We’re also able to set a default value in case the provided key doesn’t have a value.
Cypress Testing
The only specific setup thing we needed to do to enable component testing was to add the mount
command to Cypress. They have packages available for multiple common frameworks which will provide the functionality for you - all we have to do is go into our cypress/support/component.ts
file, import the mount
function, and then tell Cypress to add that command. This lets us mount our individual components so we can then run our Cypress tests directly on that component.
To enable our tests to also be able to access our provided value in the GreetView
component, we’ve customized the provided mount
function so that we provide an initial value. Then, for each test that needs to mount the component, we can either provide it with nothing (thus allowing our default value to be tested) or give it a custom message just for our tests to ensure everything works as expected.
That cypress/support/component.ts
file is also where you’ll import any global styles your components might need.
Deployment
Deploying to a hosting service like Netlify or Vercel is as straight forward as hooking up your repository to the service and letting the service auto detect the configuration for you.
The main question they’re likely to ask is what command to run to build your site, which for this kit is the build-only
command (or build
if you want it to run type checking first).