My Vue frontend starter with tests, CI/CD - everything dockerized, don't even need nodejs locally! The only requirements are docker and taskfile.
Also with a styleguide for large-scale frontend based on my experience.
If you launch for the first time:
cd .dev
task env
task init
task up
If not:
cd .dev
task up
And that's all! You are gorgeous!
You now have:
-
localhost - where your application runs.
-
localhost:8000 - where preview of your application runs.
-
localhost:8100/__vitest__/ - where your Vitest UI Mode runs.
Frontend: vue 3, typescript, xstate, tailwind, vite-ssg.
Test: vitest, playwright.
Lint: eslint with custom config (based on antfu/eslint-config) + stylelint with custom config.
-
Very detailed styleguide for large-scale frontend based on my experience.
-
E2E-tests with UI.
Superpowerful Playwright UI Mode running in docker, you don't need to install anything on your machine!
-
Ready to CI/CD to Github Actions.
Test and build on
pull request
tomaster
, deploy onpush
tomaster
. -
It's a tool that allows you to handle your logic using finite state machines which can be visualized. I am such a huge fan of it, I use it in every project. I have a styleguide for dealing with machines here.
Also check how XState can own e2e-testing here.
Invest your time into learning it, it's an absolute game changer.
-
If you encounter
ENOENT: Permission denied
error, go to.dev/.env
and replaceUSERID
with yours.It's your system user's id and group id. Usually user id (first value) is 1000 and group id (second value) is 1000 too.
Run
id -u
andid -g
in your terminal and changeUSERID
value to what you will receive. -
If you are not on Linux, you need to go to Taskfile.yaml and search for
TODO
.You will need to add some tweaks to the configuration.
-
Treat every component and page as a standalone project, domain. Try to make them as independent as possible without any external dependencies.
Store everything related to component inside it's folder (assets, composables, subcomponents etc.).
Create abstractions only when you need them, don't overengineer ahead of time. Create subcomponents only when you are absolutely sure it's necessary.
Ask yourself: "Can this component be used in another project as-is? Does it really need to be split into multiple components? Does it really need that dependency?".
-
Don't hesitate to violate DRY if you feel that it will greatly improve the readability and simplicity of the code.
For example, 3 of your components use small function, but it requires lots of dependencies. It's better to duplicate it and not make it external.
But of course, it depends.
-
Don't be shy to use
SomeVeryLongAndAwkwardLookingNames
. Instead, prioritize self-explanation of your function or component.Ask yourself: "If somebody without context will read this name, will he clearly understand what it does?".
Another bonus is that it will let you easily search files by name.
-
Don't pass objects as arguments or props, fight with temptation to do it.
Always pass primitive values, composables and components shouldn't know about the form of the object.
First of all, it makes your code much more readable. And more importantly it makes writing unit tests sooooo much easier.
Bad:
function useSomething(bigObject) { // bigObject.property, bigObject.anotherProperty }
<ChildComponent :parent="parent" />
Good:
function useSomething({ property, anotherProperty }) { // property, anotherProperty } useSomething({ property: bigObject.property, anotherProperty: bigObject.anotherProperty })
<ChildComponent :parent-property="parent.property" :another-property="parent.another" />
-
Always use vueuse. Moreover, check releases from time to time, because they add new stuff pretty often.
Use src/assets only for global assets. Create a folder for each category of assets.
Place component assets inside the component folder.
-
Naming goes from general to specific, from left to right.
Bad:
PatientProfilePage
,DropdownMessagesPatient
Good:
PageProfilePatient
,PatientMessagesDropdown
-
Each component sits in its folder, even the smallest one. The name of the folder is the name of the component and inside you have a
.vue
file, which duplicates the name. Notindex.vue
, not anything else.components/ SomeComponent/ SomeComponent.vue
-
If you are absolutely sure that your component must have subcomponents, place them in the
components
subfolder and you can omit the naming rule for them as long as they are "private" components.But think twice before creating subcomponents. Fat
<template>
is not bad, it allows you to keep things simple.You shouldn't have more than 1 level of nesting components. If you find yourself in a situation you need to dive deeper, it's a red flag you are doing something wrong.
components/ SomeComponent/ SomeComponent.vue components/ SubComponentNamedWithoutDuplicatingParentNameBecauseItIsTreatedAsPrivateComponent.vue
-
If your component requires assets, create an
assets
subfolder. Place all of the assets at one level without creating nested subfolders.components/ SomeComponent/ SomeComponent.vue assets/ some-image.png another-asset-that-lays-on-same-level.mp4
-
If your component needs composable, create a
composables
subfolder. You can name composables as you want (just remember about the self-explanation name).components/ SomeComponent/ SomeComponent.vue composables/ useOptionsForSorting.ts
-
Always create a folder, even if you are sure that there will be only one asset or one composable.
-
Import all of your "private" stuff in a relative way. It will help you easily divide global and local composables usage.
Bad:
import { useOptionForSotring } from '@/components/SomeComponent/composables/useOptionsForSorting'
Good:
import { useOptionForSorting } from './composables/useOptionsForSorting'
-
Never call something directly in
<template>
, always create your wrapper. It will serve as some kind of your internal API.Bad:
<SomeComponent @some-event="someExternalDependency.has.SomeMethod('foo')">
Good:
<SomeComponent @some-event="yourFunctionInScriptSetupWhichCallsSomeExternalDependency">
-
If you work on a shared component and listen to user events, name functions like
onInput
,onChange
(try to stick to native events naming). But in the parent component name listener by meaning in the present simple imperative mood.<SomeComponent @change="updateFilter">
-
When you want to customize some shared component, try not using props.
Instead, try utilizing attribute inheritance.
If it doesn't fit, use the power of named scoped slots to give the consumers of your component freedom to customize.
And only if nothing works, use props.
-
Don't hold any logic inside components, only emit events to the consumer even if you think it'd overhead right now.
-
Keep composables as small as possible. Avoid big composables which export lots of stuff. It can lead to lots of composables but it's ok. Name them by meaning, not by location.
Bad:
SomeComponent/ composables/ useSomeComponent.ts SomeComponent.vue
Good:
SomeComponent/ composables/ useTabs.ts useSortOptions.ts useFilters.ts SomeComponent.vue
-
Avoid fat
<script setup>
, instead divide logic into composables, not in reusability, but in organizing purpose (but remember that fat<template>
is ok). -
Don't pass arguments in order, instead take only 1 argument as an object and destructurize it (this rule applies to every function, not only composables).
Bad:
function useSomething(some, arguments, for, composable)
Good:
function useSomething({ some, arguments, for, composable })
-
Always give specific names to what composable returns.
Bad:
function useTabsOptions() { // ... return { options }
Good:
function useTabsOptions() { // ... return { tabsOptions }
-
Avoid using
enums
, prefer const assertions.const tabs = [ { name: 'Tab name', value: 'tab-value', }, { name: 'Another tab', value: 'another-tab-value', }, ] as const; export type TabValue = (typeof tabs)[number]['value'] // 'tab-value' | 'another-tab-value'
-
Types are always local and sit in the
types.ts
file.components/ SomeComponent/ SomeComponent.vue types.ts
-
Make types global only when you are sure they need to be shared (another component needs that type).
Keep them small as possible too. Lots of small files are better than one huge.
src/ components pages types/ TypePatientStatus.ts TypePatientOptions.ts
-
Group type files by content, not by domain.
Bad:
types/ TypePageLogin.ts
Good:
types/ TypeTabs.ts TypeOptions.ts
-
Group types inside file by categories.
// UI type Options = { ... // Fetch responses type FetchResponseLogin = { ...
I prefer the way when i have global singleton machines, not spawned and killed when user leaves page. It gives much more friendly DX when dealing with types, accessing context etc.
-
Try to avoid global state. Treat your machines as independent services.
If your machine needs data from another machine, try to pass that data in the moment of spawning machine. Avoid direct binding between machines.
It will cause an overhead at the beginning, but while your application will grow and become more and more complex, you will benefit from it more and more.
-
Try to store only primitives in context. It just makes your code and understanding of what your machine does much easier.
Bad:
context: { user: { name: string, data: { hp: number } } }
Good:
context: { userName: string, userHp: number }
-
Name machines with capital letter, running services - same name but with lowercase.
const MachineIndex = setup({ // ... }) const machineIndex = useMachine(MachineIndex)
-
Machine for component always has the name
MachineIndex
and is located in themachines
subfolder.components/ SomeComponent/ machines/ MachineIndex.ts SomeComponent.vue
-
Check if the machine is in some state by tags, not by state name.
Bad:
if(machineIndex.value.context.value.value === 'Some name') {
Good:
if(machineIndex.value.context.value.hasTag('some-tag') {
-
When designing machine in visual editor, form some general rules and stick to them.
For example:
All of the states are being drawn from left to right horizontally. Vertical states means another options.
You can have your own rules, but once you formed them stick to them.
-
Also create the rules about naming actions, events and states.
For example:
Events: passive verbs in past
Button was clicked
Actions: imperative verbs in present
fetchSessionInfo
States: adjectives or verbs in present
Fetching session info
You can make your own rules, but be consistent once you've formed them.
-
Keep your actions as small as possible. They must do only one thing. And do your best to keep them pure.
It can lead to overhead with 5-6 actions for 1 transition, but it's ok. It makes people without context (even non-techs if you name them right) easily understand what is going on.
Also, it makes it very easy for you to reuse action in another machine.
Bad:
actions: assignContext() { ...
Good:
actions: assignOneContextProperty() { ... assignAnotherContextProperty() { ... assignMoreContextProperty() { ...
-
Try to declare actions in
setup
in an order of their logical appearance. -
If your actions accepts some arguments, write actions that use that arguments in that event, not in the "entry" of the state.
It will help you easily visually divide inner logic from incoming.
XState has an awesome package @xstate/test, which allows you to make your machine test providers.
You can create test models from your existing machines and treat it like "unit-testing" of your machines.
However, I prefer a different approach, when you create a separate machine for e2e-testing, which describes all of the possible user interactions with flows, like MachineUserFlow.
The event-based nature of state machines just fits perfectly with how users interact with the applications.
Include layout on every page, not in your App.vue
.
It could be cumbersome, but it gives you the power of customization and controlling each page look individually.
Just create slots in layouts and override them on your page.
Treat pages as components, they can have subcomponents, types, etc.