Starter.dev is an open source community resource developed by This Dot Labs that provides code starter kits in a variety of web technologies, including React, Angular, Vue, etc. with the hope of enabling developers to bootstrap their projects quickly without having to spend time configuring tooling.
Intro
Today, we’re delighted to announce a new starter kit featuring Vue and XState! In this blog post, we’ll dive into what’s included with the kit, how to get started using it, and what makes this kit unique.
What’s included
All of our kits strive to provide you with popular and reliable frameworks and libraries, along with recommended tooling all configured for you, and designed to help you spin your projects up faster. This kit includes:
- Vue as the core JS framework
- XState for managing our application’s state
- CSS for styling
- Cypress component testing
- Vue Router to manage navigation between pages
- Storybook for visual prototyping
- ESlint and Prettier to lint and format your code
How to get started using the kit
To get started using this kit, we recommend the starter CLI tool. You can pass in the kit name directly, and the tool will guide you through naming your project, installing your dependencies, and running the app locally. Each kit comes with some sample components, so you can see how the provided tooling works together right away.
// npm
npm create @this-dot/create-starter -- --kit vue3-xstate-css
// yarn
yarn create @this-dot/create-starter --kit vue3-xstate-css
Now let’s dive into some of the unique aspects of this kit.
Vue 3
Vue is a very powerful JS framework. We chose to use Vue directly to highlight some of the features that make it such a joy to work with.
One of our favorite features is Vue’s single file components (SFC). We can include our JavaScript, HTML, and CSS for each component all in the same file. This makes it easier to keep all related code directly next to each other, making it easier to debug issues, and allowing less file flipping.
Since we’re in Vue 3, we’re also able to make use of the new Composition API, which looks and feels a bit more like vanilla JavaScript. You can import files, create functions, and do most anything you could in regular JavaScript within your component’s script tag. Any variable name you create is automatically available within your HTML template.
Provide and Inject
Another feature we got to use specifically in this starter kit is the new provide and inject functionality. You can read more details about this in the Vue docs, but this feature gives us a way to avoid prop drilling and provide values directly where they’re needed.
In this starter kit, we include a “greeting” example, which makes an API call using a provided message, and shows the user a generated greeting. Initially, we provided this message as a prop through the router to the greeting component. This works, but it did require us to do a little more legwork to provide a fallback value, as well as needing our router to be aware of the prop.
Using the provide / inject setup, we’re able to provide our message through the root level of the app, making it globally available to any child component. Then, when we need to use it in our GreetView
component, we inject the “key” we expect (our message), and it provides a built-in way for us to provide a default value to use as a fallback. Now our router doesn’t need to do any prop handling! And our component consistently works with the provided value or offers our default if something goes wrong.
// src/main.ts
const app = createApp(App);
app.provide('query', 'from This Dot Labs!');
// src/views/GreetView.vue
<script setup lang="ts">
import { inject } from 'vue';
const providedQuery = inject('query', '');
const { state } = useMachine(greetMachine(providedQuery));
</script>
Using XState
If you haven’t had a chance to look into XState before, we highly recommend checking out their documentation. They have a great intro to state machines and state charts that explains the concepts really well.
One of the biggest mindset shifts that happens when you work with state machines is that they really help you think through how your application should work. State machines make you think explicitly through the different modes or “states” your application can get into, and what actions or side effects should happen in those states. By thinking directly through these, it helps you avoid sneaky edge cases and mutations you don’t expect.
Difference between Context and State
One of the parts that can be a little confusing at first is the difference between “state” and “context” when it comes to state machines. It can be easy to think of state as any values you store in your application that you want to persist between components or paths, and that can be accurate. However, with XState, a “state” is really more the idea of what configurations your app can be in.
A common example is a music player. Your player can be in an “off” state, a “playing” state, or a “paused” state. These are all different modes, if you will, that can happen when your music player is interacted with. They can be values in a way, but they’re really more like the versions of your interface that you want to exist. You can transition between states, but when you go back to a specific state, you expect everything to behave the same way each time. States can trigger changes to your data or make API calls, but each time you enter or leave a state, you should be able to see the same actions occur. They give you stability and help prevent hidden edge cases.
Values that we normally think of as state, things like strings or numbers or objects, that might change as your application is interacted with. These are the values that are stored in the “context” within XState. Our context values are the pieces of our application that are quantitative and that we expect will change as our application is working.
export const counterMachine = createMachine(
{
id: 'Counter',
initial: 'active',
context: {
count: 0,
},
states: {
active: {
on: {
INC: { actions: 'increment' },
DEC: { actions: 'decrement' },
RESET: { actions: 'reset' },
},
},
},
},
{
actions: {
increment: assign({ count: (context) => context.count + 1 }),
decrement: assign({ count: (context) => context.count - 1 }),
reset: assign({ count: (context) => (context.count = 0) }),
},
}
);
Declaring Actions and Services
When we create a state machine with XState, it accepts two values- a config object and an options object. The config tells us what the machine does. This is where we define our states and transitions. In the options object, we can provide more information on how the machine does things, including logic for guards, actions, and effects.
You can write your actions and effect logic within the state that initiates those calls, which can be great for getting the machine working in the beginning. However, it’s recommended to make those into named functions within the options object, making it easier to debug issues and improving the readability for how our machine works.
Cypress Testing
The last interesting thing we’d like to talk about is our setup for using component testing in Cypress! To use their component testing feature, they provide you with a mount command, which handles mounting your individual components onto their test runner so you can unit test them in isolation.
While this works great out of the box, there’s also a way to customize the mount command if you need to! This is where you’d want to add any configuration your application needs to work properly in a testing setup. Things like routing and state management setups would get added to this function.
Since we made use of Vue’s provide and inject functions, we needed to add the provided value to our mount
command in order for our greeting test to properly work. With that set up, we can allow it to provide our default empty string for tests that don’t need to worry about injecting a value (or when we specifically want to test our default value), and then we can inject the value we want in the tests that do need a specific value!
// cypress/support/component.ts
Cypress.Commands.add('mount', (component, options = {}) => {
options.global = options.global || {};
options.global.provide = options.global.provide || {};
return mount(component, options);
});
Conclusion
We hope you enjoy using this starter kit! We’ve touched a bit on the benefits of using Vue 3, how XState keeps our application working as we expect, and how we can test our components with Cypress.
Have a request or a question about a [starter.dev] project? Reach out in the issues to make your requests or ask us your questions. The project is 100% open sourced so feel free to hop in and code with us!