Our Vue guidelines
Working with multiple people in a start-up project requires good communication and some conventions to accept to scale successfully and match the product need in productivity. However, because almost infinite possibilities exist to code the same feature, we need to get our stories straight and create guidelines that are easy to follow, which benefits the team's efficiency but also helps in delivering high-quality features, easy maintenance, and good readability.
Our vision
According to the need, we started to create the core principles that we believe will make important improvements in the quality of our deliverables. We have 3 core principles that are very close to each other and will lead our thoughts on changing/adding new good practices.
Component’s independence
The first principle is that we should be able to create independent components that are - in theory - easy to extract from one project to another. This is the initial goal of the component system in a front-end framework like VueJs. We want to keep this ideology and use it across our daily code.
Creating components will help in code readability and also help us to factorize and avoid having code duplicates. Because having twice the same code means that if you want to change it, you have to change it everywhere, and also, the same if you want to remove it or improve it.
Despite the fact that a more independent component can be easily extracted, it also means that we reduce the things that will impact our own components, which makes it easier to maintain. This does not mean that we don’t create relations between our files, in fact, we do it a lot but we try to keep a simple communication, easy to understand, and easy to find between our component and external impacts like API, props inheritance, compositions, etc…
One example that would conflict with this vision would have been if we created an entire page in a single component. Data are being fetched from one place (Store, API call), then treated from another place. Admitting we are receiving the data (helpers or libraries) and then received in our component by inheritance (mixins). Hence, everything is magic and nothing tells you in this component that you have the data you want, formatted the way you want unless to walk through the whole path.
Smart & dumb components
The second pillar is smart and dumb components which can be a little bit different from what you can read about those terms elsewhere.
In our case, our dumbest components are when it has no business logic inside, its goal is to show some data he doesn’t even know about. Most of our UI components like buttons or modals are dumb components.
We call a component smarter when it adds strong context or business logic. Most of the time, it is built from dumber components assembled with context or specific data. For example, we can imagine a component that renders a card for a premium member with their name, and avatar, … So it is built from an avatar and a card (dumb) component and thanks to its context, creates a member card.
Doing that means that our dumbest components need a lot of effort in the making because they need to be very adaptive and robust to be used everywhere. The smarter the component is, the easier it is to create because it is just placing some already existing component and matching it with our context.
As you can see it’s not a two-sided concept, a component is not either dumb or smart. It is just smarter than one and dumber than another so we can think of that as a “smart scale”. A generic button is dumber than an “add to cart” shopping button, which is dumber than an order card with this button, which is dumber than a shopping cart page.
Black box design
The last principle wraps up the first 2. Even if dumb components might be complex to create, we still need to be easy to work with inside our application. To do that, we have commonly agreed that we don’t need to know how the component works inside, we just want to use it so we need the communication interface contract with it to be crystal clear and self-documents the component itself. After that, all devs should be able to use it freely and have what they expect.
Of course, this concept doesn’t work only for the dumbest components but for any components, and this is why we have a Storybook design system displaying our available components to make it much simpler to find and use the correct components.
Finally, we can imagine that if we need to work inside a dumb component which is a black box, it would be super hard to get in and we would need a front-end expert but due to the “Component’s independence” and “Smart & dumb design”, we believe it’s not that complicate to work inside and make improvements and we also added some extra guidelines to ease it a little more.
Matching Vue trends with our vision
In order to help the readability of our code, we have chosen to follow the Vue trends proposed by its community and adapted to the previous principles.
Strict Vue ESLint rules
At Jolimoi, we have decided to have a very strict technical environment and guidelines and we chose ESLint to handle most of the job for us. This way, most of the code is produced with the same conventions so we are more efficient in reading and understanding the code already in place.
So we decided to use Vue ESLint rules from their documentation and apply all the rules possible inside our code base. We applied all base, essential, strongly recommended, recommended, and uncategorized rules to our linter. Some of them were incompatible with our way of coding so we chose to disable them for now and we also configured some of the rules with the recommended settings most of the time. We are in the middle of a rewriting migration so we are adding these rules little by little in our codebase, but this is the goal we are aiming for.
Once it’s in place, and of course it breaks our CD/CI if we have any errors during the linting, all developers use it and agree with the code guidelines. For example, Vue gives us rules to forbid the creation of components of one word only. Every component must be at least 2 words combined in order to differentiate between HTML tags and Vue components, so it’s easier to see/read. We also have rules to disallow us to create anti-patterns like mutating props, or to forbid having a v-if at the same level as a v-for.
Composition API over Option API
Also, Vue appeals to us to use more composition API, especially with the script setup syntactic sugar. We wanted to use composition API because it adds much more possibilities to manipulate our code outside/across our Vue components which gets very important when you want to factorize your code and avoid duplicates.
Also, we believe that using composition is a good option to handle impacts coming from outside a component since it’s clear enough to understand where it comes from (and easy to navigate through it with a code editor) . Also, using composable for example, we can “scope” our impact and also get only the minimum required for our component to work properly, which is super compatible with our vision.
So we have decided to switch to composition API and script setup. In fact, it’s a simple conversion that even ChatGPT can do for us. On top of that, we have chosen a few guidelines to ease the skill improvement such as choosing ref() over reactive(). Ref() may be trickier than reactive() but works in all scenario and we believe that it helps us to understand better how Vue work. Also, we have decided to organize the most efficient way our code and group them: declarative variables (such as props, composable, emit), const/data, computed, methods, lifecycle methods, etc. so it’s easier to read and find what’s affecting what inside our component.
Also, since we are not in an object anymore, it feels obvious that having a huge component with a lot of code and complexity inside is getting very hard to read and maintain so we are more encouraged to extract code outside, especially with the full power of composition API.
For example, we have a feature flagging library that we want it to disable, regarding a tag which is a boolean get from Firebase, a specific part of our codebase. Previously we had a mixin given to every component in our application that initialized our firebase got all of the feature flags and then populate asynchronously some methods and data. It works well but we have a few problems:
- since it’s asynchronous, it’s difficult to evaluate whether the data is passed or not
- by looking at our component, it is impossible to know that we have these properties inside which makes it hard for newcomers to start working on the stack
- it’s impossible to use these feature flag checks outside a Vue component as it is today
- since we now want to use script setup, mixins are no longer compatible
So a better way to handle this would be to rewrite the mixin into a composable with something like const { isFeatureEnabled } = useFeatureFlag() so now, inside a component if we want to use it we import it so we know where it comes from and if we want to use it outside the component, we can.
Minimize store usage
We believe that using a global store like Vuex or Pinia is a core-breaking principle since it creates impact across all our components while nowadays, we can do almost everything we commonly do inside a Vue store with composition API to communicate across multiple components inside our application.
Most developers use Vue store in order to either store data we want to use across the entire application or store data in order to cache it. For the 2 needs, we have an answer: we use composition API and composable so we can use data across the app. For caching, the only thing we’d like to cache right now is API calls, and for that, Tanstack/query does the job for us perfectly.
For example, we like to use Vue store for authentication across the application and to have the user everywhere but with composition API, we can handle it super easily without any other libraries. We created a useAuth() composable that handles everything for us, which is Vue-reactive and can be easy either inside any components and also outside. Of course, it handles both sides: get the currently logged-in user but also some actions such as login or logout.
Conclusion
To conclude, all the guidelines you just read are a vision, it’s more of a goal to reach but it can happen that sometimes, for various reasons, we do “bad code” that doesn’t follow every guideline here. We want to involve developers to think of what they create and especially why they create it this way. Doing this helps us to decrease the chances of having struggles to maintain some code later.
We just believe that keeping our mind on this will help the product and the dev team to improve and it will ease our daily tasks.