<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Vue on Smashing Magazine — For Web Designers And Developers</title><link>https://www.smashingmagazine.com/category/vue/index.xml</link><description>Recent content in Vue on Smashing Magazine — For Web Designers And Developers</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Mon, 09 Feb 2026 03:03:08 +0000</lastBuildDate><item><author>Joran Quinten</author><title>How To Deal With Big Tooling Upgrades In Large Organizations</title><link>https://www.smashingmagazine.com/2023/05/big-tooling-upgrades-large-organizations/</link><pubDate>Wed, 17 May 2023 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2023/05/big-tooling-upgrades-large-organizations/</guid><description>The process of upgrading large third-party packages in equally large organizations is rarely, if ever, as easy as running an npm update and calling it a day in this context. Joran Quinten shares valuable lessons from his team’s experiences in upgrading third-party code that affects codebases across the entire organization.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2023/05/big-tooling-upgrades-large-organizations/" />
              <title>How To Deal With Big Tooling Upgrades In Large Organizations</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>How To Deal With Big Tooling Upgrades In Large Organizations</h1>
                  
                    
                    <address>Joran Quinten</address>
                  
                  <time datetime="2023-05-17T10:00:00&#43;00:00" class="op-published">2023-05-17T10:00:00+00:00</time>
                  <time datetime="2023-05-17T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>If you work in software development, you probably know a thing or two about using and maintaining third-party packages. While third-party tooling has its fair share of downsides, there are plenty of advantages as well. The efficiency you get from code that someone else has already written speeds up development and is hard to deny. Sure, there are all sorts of considerations to take in before plopping code from a third party &mdash; accessibility, technical debt, and security, to name a few &mdash; but the benefits may make taking on those considerations worthwhile for your team.</p>

<p>Upgrades are also part of that set of considerations. Usually, your team may treat this sort of maintenance as a simple task or chore: upgrading dependencies and (automatically) validating that all of the features keep functioning as expected. You probably even have automated checks for keeping all package versions up to date.</p>

<p>But what if the third-party tooling you adopt is big? I mean <em>big</em>, big. That’s common in large organizations. I happen to work for a fairly large organization that leverages big third-party resources, and <strong>upgrading those tools is never as simple as running a package update and moving on</strong>. I thought I’d share what’s involved in that process because there are many moving pieces that require ample planning, strategy, and coordination. Our team has learned a lot about the process that I hope will benefit you and your team as well.</p>

<h2 id="some-context-on-my-organization">Some Context On My Organization</h2>

<p>I work for Jumbo Supermarkten in the Jumbo Tech Campus (JTC), which is a department of over 350 developers working in agile teams on a range of digital products that help facilitate our core grocery and e-commerce processes.</p>

<p>We have a variety of responsibilities, where 70% of the work is allocated to the primary objectives for each team, and the remaining 30% is dedicated to anything a team wants, as long as it is beneficial to the JTC, which is very useful if you want to deliver value outside of your own team.</p>

<p>When we look at maintaining tooling and packages, balancing the goals of each team with the goals of JTC means that teams effectively maintain their own codebases while also collectively maintaining internally shared codebases that serve as the tooling and foundation of our applications.</p>

<h2 id="centralized-code-as-a-bottleneck">Centralized Code As A Bottleneck</h2>

<p>To build our applications with consistent standards, we rely on an internal design system and the component library we call Kompas (Dutch for “Compass”). We have built this system ourselves and rely on <a href="https://vuejs.org">Vue</a> to render our components and build interactions. <strong>Kompas is a hard dependency for virtually all of our applications to ensure uniformity.</strong></p>

<p>This project was not allocated to a dedicated team. Instead, we adopted a strategy that introduced plenty of guidance to allow all front-end developers to contribute. Any developer can add new components to the library as well as features to existing components and keep everything in sync with the designs.</p>

<p>Teams normally work on business features since product owners love delivering customer value. The way we set up our process would allow a team to, in one sprint:</p>

<ul>
<li>Make the required change in Kompas,</li>
<li>Have it reviewed by peers from both inside and outside a particular team,</li>
<li>Publish the latest version of the component library, and</li>
<li>Use that version in that team’s own application to deliver to the end user.</li>
</ul>

<p>We can only do this with automation on repetitive processes &mdash; linting, formatting, quality assurance, testing, visual comparisons, and publishing &mdash; in order to provide enough room for developers to contribute to the process. Our component library is very much <strong>a living document of our design system</strong>, with multiple minor releases and patches a week. With <a href="https://semver.org">semantic versioning</a>, we can keep our own applications up to date easily and with confidence.</p>

<p>For bigger undertakings, such as setting up visual snapshot tests, we established temporary working groups alongside our existing teams that we called “front-end chapters” where members join on a voluntary basis. In these meetings, we discuss what needs to be done, and in the available 30% of free time we are allotted, the members of these teams carry out the work and report back to the chapter.</p>

<p>As you can imagine, we’ve spent a lot of time and effort ensuring the quality and making it a reliable part of our landscape.</p>

<p>This all began when Vue was in Version 2. That’s the version we baked into Kompas, which means we effectively forced our whole application landscape to follow suit. This worked perfectly for us; people could focus on their team’s needs while leaning on the support of the entire front-end chapter that works on Kompas.</p>

<p>Following the Vue ecosystem that we introduced, Vuex and Nuxt became part of our environment. And then <a href="https://github.com/vuejs/core/releases/tag/v3.0.0">Vue 3</a> was announced, and it was a massive breaking change from Vue 2! With the announcement, the end-of-life date for Vue 2 was set for December 31, 2023. We still have some time as of this writing, but the news had a massive impact that cascaded throughout our organization.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="we-needed-a-strategy">We Needed A Strategy</h2>

<p>We needed to upgrade Vue from 2 to 3. The first thing that we needed to figure out was when we could reasonably start the process. To assess and strategize, we formed a small virtual team of developers consisting of members from various teams so that multiple perspectives were represented.</p>

<p>We figured that there would be a period of time when we would need to support both versions in order to allow time for migrating between teams. It would be nearly impossible to orchestrate a monolithic release. Thus, we <strong>prefer gradual incrementing over massive sweeping changes</strong>. On the other hand, having to maintain two versions of Vue for, basically, the same business feature presented costs in time and complexity.</p>

<p>So, in order to execute this process as responsibly as possible, we set out to figure out <em>when</em> we could start, taking into account the longevity of maintaining two codebases while getting early experience from upgrading. We started to <strong>map the different tech stacks for each team</strong> and plotted out <strong>potential bottlenecks</strong> for the sake of making the process of our work as widely visible as possible. At this time, our organization had a very flat structure, so we also needed to get internal stakeholders (i.e., product owners, architects, and managers) involved and convey the effect this upgrade would have on teams across the board.</p>

<h2 id="creating-a-map">Creating A Map</h2>

<p>With our starting point set, we move on to establish a direction. Not having a dedicated team did pose some challenges because it meant that we needed to align everybody in a democratic way. This is, in Dutch culture, also known as <em>polderen</em>:</p>

<blockquote>We try to find consensus in a way where everybody is equally happy, or unhappy, about the final direction.</blockquote>

<p>And this can be challenging in a department that consists of many cultures!</p>

<p>One thing we knew we could rely on was the published best practices from official Vue resources to guide our decision-making process. Referencing the documentation, we did notice opportunities for incremental upgrades. The release of <a href="https://blog.vuejs.org/posts/vue-2-7-naruto.html">Vue 2.7 (Naruto)</a> was really helpful in the sense that it backported features from Vue 3 back to a Vue 2-compatible version.</p>

<p>We also noted that in our landscape, not all applications were actually using Nuxt. A stable release of Nuxt 3 would be a prerequisite for those applications to even be considered for migration since the Vue major version is tightly coupled with the Nuxt major version. Luckily, some applications in our landscape are standalone Vue apps. These are ideal candidates for the first Vue 3-compatible components.</p>

<p>But first, we would need to have components that were compatible with Vue 3.</p>

<div class="partners__lead-place"></div>

<h2 id="the-big-divide">The Big Divide</h2>

<p>By this point, we were confident enough to get to work. We had a plan and clear strategy, after all. The first order of business was to make sure that our component library was compatible with Vue 3, preferably while minimizing duplicative efforts.</p>

<p>We found a really nice way of doing this:</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aWe%20created%20a%20new%20workspace%20called%20%e2%80%9cKompas-next%e2%80%9d%20next%20to%20the%20regular%20components%20folder,%20which%20was%20scaffolded%20out%20using%20Vue%203.%20Then%20we%20imported%20the%20components%20from%20the%20original%20library.%20%0a&url=https://smashingmagazine.com%2f2023%2f05%2fbig-tooling-upgrades-large-organizations%2f">
      
We created a new workspace called “Kompas-next” next to the regular components folder, which was scaffolded out using Vue 3. Then we imported the components from the original library. 

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>This only works because:</p>

<ul>
<li>The backported features in Vue 2.7 allowed us to move closer toward the Vue 3 composition API (among other things).</li>
<li>The component syntax between Vue 2 and Vue 3 isn’t radically different anymore.</li>
<li><a href="https://github.com/vueuse/vue-demi">Vue Demi</a> allowed us to convert components, one by one, to be compatible with both versions.</li>
<li>We made sure that Kompas-next runs isolated tests to ensure stability.</li>
</ul>

<p>We did have to slightly modify each and every component to adapt to the new standards. We’ll get to that process in a minute.</p>

<p>That said, we were able to publish two versions of our component library: one that is compatible with Vue 2 (Kompas) and one that is compatible with Vue 3 (Kompas-next). This, in turn, meant that the teams that did not have Nuxt as a dependency could potentially start migrating!</p>

<h2 id="getting-organized">Getting Organized</h2>

<p>Up to this point, most of the groundwork had been done in a <em>relatively</em> small team. We were in charge of the investigations, communication, and alignment. But we still needed to get stuff done &mdash; a lot of stuff!</p>

<p>With every developer being able to contribute, we came to an agreement that fits with the way everybody was already contributing to the component library:</p>

<blockquote>If you touch a component that is not yet compatible, convert it to be compliant with both Vue 2 and Vue 3 using Vue-demi. Add the existing component with tests to the imports of the Kompas-next folder.</blockquote>

<p>Having communicated this strategy early in the process, we immediately saw the Kompas-next library growing. The Vue core team has put so much effort into closing the gap between the two versions, which made our lives much easier.</p>

<h2 id="feedback-from-early-adopters">Feedback From Early Adopters</h2>

<p>The teams that were not blocked by a Nuxt 3 release could spend their time migrating their complete app to Vue 3, providing feedback along the way on how we were setting up our packages and converting components.</p>

<p>Seeing the first applications using Vue 3 was a milestone we could all be proud of since we managed to reach it together, collaboratively, and with a united strategy. The strategy worked for us because it closely resembled the way we were already working.</p>

<p>There were indeed some components that were not migrated using this strategy, which indicated to us that they were stale in terms of development. We reasoned that “stale” equals “stable” and that it would be perfectly fine to migrate them by manual assignment and distribution since we can expect it to be close to a one-off migration per component.</p>

<p>We also started to add Vue 3-specific capabilities to our component library, such as our own <a href="https://vuejs.org/guide/reusability/composables.html">composables</a>. I think that’s a nice testament to the investment and adoption by our front-end chapter.</p>

<p>With the component library now supporting Vue, we cleared a significant migration hurdle in our organization. We enabled teams to start migrating to Vue 3, and we encouraged new applications to use the latest standards. As a result, we could start thinking about a deprecation path for our Vue 2 codebase. We were cautiously optimistic and aligned the end-of-life date for Kompas with the same date for Vue 2: December 31, 2023.</p>

<p>So, yes, we are not yet finished and still have work to do. In fact, we had…</p>

<h2 id="two-minor-elephants-in-the-room">Two (Minor) Elephants In The Room</h2>

<p>To support communication between micro-applications that run on our e-commerce domain, we had resorted to using <a href="https://vuex.vuejs.org">Vuex</a> in the past. We used to register stores globally so other applications could dispatch actions and retrieve a shared state. This is now gradually being migrated in the sense that we are replacing Vuex with <a href="https://pinia.vuejs.org">Pinia</a> for internal state management.</p>

<p>For cross-app communication, we are in the process of decoupling Vuex as an external interface and promoting the use of custom events tied to a specific domain. This prevents us from locking ourselves out of future state management tooling.</p>

<p>We are also in the process of preparing our Nuxt applications to be cleared for migration as well. Within our e-commerce domain, we’ve been building specific modules that take a lot of overhead out of our hands: They handle tasks like setting meta headers, security, and analytics. These are being rewritten to use plugins rather than modules. The impact of this breaking change is smaller because it is limited to the teams that use these modules. We see that these teams are using a similar strategy, albeit on a smaller scale, to organize and structure the tasks at hand.</p>

<div class="partners__lead-place"></div>

<h2 id="looking-back">Looking Back</h2>

<p>I believe we have a few key takeaways from how we upgraded (and continue to upgrade) from one version of a large third-party resource to another within our large network of teams and shared codebases. I believe the lessons we learned are relevant beyond Vue and can be applied to the processes of other large organizations migrating between versions of a core piece of architecture.</p>

<p>Let’s review what we learned:</p>

<ul>
<li><strong>Ensure the transition period is clear and as short as possible</strong>.</li>
<li><strong>Facilitate breaking the work down into small steps</strong> that progress iteratively and solicit feedback from those involved in the process as early and as often as possible.</li>
<li><strong>Onboard key stakeholders</strong> to make sure your team has ample time and resources to do the work.</li>
<li><strong>Define a strategy</strong> that fits with your organization’s culture.</li>
<li><strong>Foster a collaborative mindset</strong> and establish clear communication between teams.</li>
<li><strong>Celebrate wins</strong>, even the smallest ones!</li>
</ul>

<h2 id="the-work-is-never-done-really">The Work Is Never Done, Really</h2>

<p>As I mentioned earlier, <strong>maintenance is a never-ending piece of the software development process</strong>. As Vue creator Evan You stated in the <a href="https://www.youtube.com/watch?v=I5mGNB-4f0o">State of the Vuenion 2023</a>, Vue plans to ship more frequent updates and releases. This will keep impacting our work, but that’s okay. We have a plan and blueprint for future releases.</p>

<p>We’re not there yet, but we now know how to get there!</p>

<h3 id="further-reading-on-smashingmag">Further Reading On SmashingMag</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2021/11/maintain-large-nextjs-application/">How To Maintain A Large Next.js Application</a>, Nirmalya Ghosh</li>
<li><a href="https://www.smashingmagazine.com/2023/03/vue-case-study-migrating-headless-cms-system/">Moving From Vue 1 To Vue 2 To Vue 3: A Case Study Of Migrating A Headless CMS System</a>, Lisi Linhart</li>
<li><a href="https://www.smashingmagazine.com/2022/11/optimizing-vue-app/">Optimizing A Vue App</a>, Michelle Barker</li>
<li><a href="https://www.smashingmagazine.com/2019/02/web-app-maintenance/">Why Web Application Maintenance Should Be More Of A Thing</a>, Darren Beale</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk, il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Olufunke Moronfolu</author><title>Building Complex Forms In Vue</title><link>https://www.smashingmagazine.com/2023/03/building-complex-forms-vue/</link><pubDate>Thu, 09 Mar 2023 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2023/03/building-complex-forms-vue/</guid><description>Chances are, we will have to build a complex form at least once in our software engineering journey. This article goes over creating a complex form that can be progressively enhanced using some Vue features like the &lt;code>v-for&lt;/code> and the &lt;code>v-model&lt;/code>. It also gives a refresher on some basic Vue core features that will come in handy when building out the complex form in your day-to-day Vue usage.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2023/03/building-complex-forms-vue/" />
              <title>Building Complex Forms In Vue</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Building Complex Forms In Vue</h1>
                  
                    
                    <address>Olufunke Moronfolu</address>
                  
                  <time datetime="2023-03-09T10:00:00&#43;00:00" class="op-published">2023-03-09T10:00:00+00:00</time>
                  <time datetime="2023-03-09T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>More often than not, web engineers always have causes to build out forms, from simple to complex. It is also a familiar pain in the shoe for engineers how fast codebases get incredibly messy and incongruously lengthy when building large and complex forms. Thus begging the question, “How can this be optimized?”.</p>

<p>Consider a business scenario where we need to build a waitlist that captures the name and email. This scenario only requires two/three input fields, as the case may be, and could be added swiftly with little to no hassle. Now, let us consider a different business scenario where users need to fill out a form with ten input fields in 5 sections. Writing 50 input fields isn’t just a tiring job for the Engineer but also a waste of great technical time. More so, it goes against the infamous “Don’t Repeat Yourself” (DRY) principle.</p>

<p>In this article, we will focus on learning to use the Vue components, the <code>v-model</code> directive, and the Vue props to build complex forms in Vue.</p>

<h2 id="the-v-model-directive-in-vue">The <code>v-model</code> Directive In Vue</h2>

<p>Vue has several unique HTML attributes called directives, which are prefixed with the <code>v-</code>. These directives perform different functions, from rendering data in the DOM to manipulating data.</p>

<p>The <code>v-model</code> is one such directive, and it is responsible for two-way data binding between the form input value and the value stored in the <code>data</code> property. The <code>v-model</code> works with any input element, such as the <code>input</code> or the <code>select</code> elements. Under the hood, it combines the inputted input value and the corresponding change event listener like the following:</p>

<pre><code class="language-html">&lt;!-- Input element --&gt;
&lt;input v-model="inputValue" type="text"&gt;

&lt;!-- Select element --&gt;
&lt;select v-model="selectedValue"&gt;
  &lt;option value=""&gt;Please select the right option&lt;/option&gt;
  &lt;option&gt;A&lt;/option&gt;
  &lt;option&gt;B&lt;/option&gt;
  &lt;option&gt;C&lt;/option&gt;
&lt;/select&gt;
</code></pre>

<p>The <code>input</code> event is used for the <code>&lt;input type= &quot;text&quot;&gt;</code> element. Likewise, for the <code>&lt;select&gt; … &lt;/select&gt;</code>, <code>&lt;input type= &quot;checkbox&quot;&gt;</code> and <code>&lt;input type= &quot;radio&quot;&gt;</code>, the <code>v-model</code> will, in turn, match the values to a <code>change</code> event.</p>

<h2 id="components-in-vue">Components In Vue</h2>

<p>Reusability is one of the core principles of Software Engineering, emphasizing on using existing software features or assets in a software project for reasons ranging from minimizing development time to saving cost.</p>

<p>One of the ways we observe reusability in Vue is through the use of components. Vue components are reusable and modular interfaces with their own logic and custom content. Even though they can be nested within each other just as a regular HTML element, they can also work in isolation.</p>

<p>Vue components can be built in two ways as follows:</p>

<ul>
<li>Without the build step,</li>
<li>With the build step.</li>
</ul>

<h3 id="without-the-build-step">Without The Build Step</h3>

<p>Vue components can be created without using the <strong>Vue Command Line Interface (CLI)</strong>. This component creation method defines a JavaScript object in a Vue instance options property. In the code block below, we inlined a JavaScript string that Vue parses on the fly.</p>

<pre><code class="language-html">template: `
  &lt;p&gt; Vue component without the build step &lt;/p&gt;
  `
</code></pre>

<h3 id="with-the-build-step">With The Build Step</h3>

<p>Creating components using the build step involves using <a href="https://vitejs.dev/">Vite</a> &mdash; a blazingly fast, lightweight build tool. Using the build step to create a Vue component makes a <strong>Single File Component (SFC)</strong>, as it can cater to the file’s logic, content, and styling.</p>

<pre><code class="language-html">&lt;template&gt;
  &lt;p&gt; Vue component with the build step &lt;/p&gt;
&lt;/template&gt;
</code></pre>

<p>In the above code, we have the <code>&lt;p&gt;</code> tag within the HTML <code>&lt;template&gt;</code> tag, which gets rendered when we use a build step for the application.</p>

<h3 id="registering-vue-components">Registering Vue Components</h3>

<p>Creating a Vue component is the first step of reusability and modularity in Vue. Next is the registration and actual usage of the created Vue component.</p>

<p>Vue components allow the nesting of components within components and, even more, the nesting of components within a global or parent component.</p>

<p>Let’s consider that we stored the component we created using the build step in a <code>BuildStep.vue</code> file. To make this component available for usage, we will import it into another Vue component or a <code>.vue</code>, such as the root entry file. After importing this component, we can then register the component name in the <code>components</code> option property, thus making the component available as an HTML tag. While this HTML tag will have a custom name, the Vue engine will parse them as valid HTML and render them successfully in the browser.</p>

<pre><code class="language-html">&lt;!-- App.vue --&gt;
&lt;template&gt;
  &lt;div&gt;
    &lt;BuildStep /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import BuildStep from './BuildStep.vue'

export default {
  components: {
    BuildStep
  }
}
&lt;/script&gt;
</code></pre>

<p>From the above, we imported the <code>BuildStep.vue</code> component into the <code>App.vue</code> file, registered it in the <code>components</code> option property, and then declared it within our HTML template as <code>&lt;BuildStep /&gt;</code>.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p><p>Meet <a data-instant href="/the-smashing-newsletter/"><strong>Smashing Email Newsletter</strong></a> with useful tips on front-end, design &amp; UX. Subscribe and <strong>get “Smart Interface Design Checklists”</strong> &mdash; a <strong>free PDF deck</strong> with 150+ questions to ask yourself when designing and building almost <em>anything</em>.</p><div><section class="nlbf"><form action="//smashingmagazine.us1.list-manage.com/subscribe/post?u=16b832d9ad4b28edf261f34df&amp;id=a1666656e0" method="post"><div class="nlbwrapper"><label for="mce-EMAIL-hp" class="sr-only">Your (smashing) email</label><div class="nlbgroup"><input type="email" name="EMAIL" class="nlbf-email" id="mce-EMAIL-hp" placeholder="Your email">
<input type="submit" value="Meow!" name="subscribe" class="nlbf-button"></div></div></form><style>.c-garfield-the-cat .nlbwrapper{margin-bottom: 0;}.nlbf{display:flex;padding-bottom:.25em;padding-top:.5em;text-align:center;letter-spacing:-.5px;color:#fff;font-size:1.15em}.nlbgroup:hover{box-shadow:0 1px 7px -5px rgba(50,50,93,.25),0 3px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025)}.nlbf .nlbf-button,.nlbf .nlbf-email{flex-grow:1;flex-shrink:0;width:auto;margin:0;padding:.75em 1em;border:0;border-radius:11px;background:#fff;font-size:1em;box-shadow:none}.promo-box .nlbf-button:focus,.promo-box input.nlbf-email:active,.promo-box input.nlbf-email:focus{box-shadow:none}.nlbf-button:-ms-input-placeholder,.nlbf-email:-ms-input-placeholder{color:#777;font-style:italic}.nlbf-button::-webkit-input-placeholder,.nlbf-email::-webkit-input-placeholder{color:#777;font-style:italic}.nlbf-button:-ms-input-placeholder,.nlbf-button::-moz-placeholder,.nlbf-button::placeholder,.nlbf-email:-ms-input-placeholder,.nlbf-email::-moz-placeholder,.nlbf-email::placeholder{color:#777;font-style:italic}.nlbf .nlbf-button{transition:all .2s ease-in-out;color:#fff;background-color:#0168b8;font-weight:700;box-shadow:0 1px 1px rgba(0,0,0,.3);width:100%;border:0;border-left:1px solid #ddd;flex:2;border-top-left-radius:0;border-bottom-left-radius:0}.nlbf .nlbf-email{border-top-right-radius:0;border-bottom-right-radius:0;width:100%;flex:4;min-width:150px}@media all and (max-width:650px){.nlbf .nlbgroup{flex-wrap:wrap;box-shadow:none}.nlbf .nlbf-button,.nlbf .nlbf-email{border-radius:11px;border-left:none}.nlbf .nlbf-email{box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);min-width:100%}.nlbf .nlbf-button{margin-top:1em;box-shadow:0 1px 1px rgba(0,0,0,.5)}}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus,.nlbf .nlbf-button:hover{cursor:pointer;color:#fff;background-color:#0168b8;border-color:#dadada;box-shadow:0 1px 1px rgba(0,0,0,.3)}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus{outline:0!important;text-shadow:1px 1px 1px rgba(0,0,0,.3);box-shadow:inset 0 3px 3px rgba(0,0,0,.3)}.nlbgroup{display:flex;box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);border-radius:11px;transition:box-shadow .2s ease-in-out}.nlbwrapper{display:flex;flex-direction:column;justify-content:center}.nlbf form{width:100%}.nlbf .nlbgroup{margin:0}.nlbcaption{font-size:.9em;line-height:1.5em;color:#fff;border-radius:11px;padding:.5em 1em;display:inline-block;background-color:#0067b859;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.wf-loaded-stage2 .nlbf .nlbf-button{font-family:Mija}.mts{margin-top: 5px !important;}.mbn{margin-bottom: 0 !important;}</style></section><p class="mts mbn"><small class="promo-box__footer mtm block grey"><em>Once a week. Useful tips on <a href="https://www.smashingmagazine.com/the-smashing-newsletter/">front-end &amp; UX</a>. Trusted by 207.000 friendly folks.</em></small></p></div></p>
</div>
</div>
<div class="feature-panel-right-col">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-firechat.svg"
    alt="Feature Panel"
    width="310"
    height="400"
/>

</div>

<p></div>
</aside>
</div></p>

<h2 id="vue-props">Vue Props</h2>

<p>Vue props, otherwise known as properties, are custom-made attributes used on a component for passing data from the parent component to the child component(s). A case where props can come in handy is when we need a component with different content but a constant visual layout, considering a component can have as many props as possible.</p>

<p>The Vue prop has a one-way data flow, i.e., from the parent to the child component. Thus, the parent component owns the data, and the child component cannot modify the data. Instead, the child component can emit events that the parent component can record.</p>

<h3 id="props-declaration-in-vue">Props Declaration In Vue</h3>

<p>Let us consider the code block below:</p>

<pre><code class="language-html">&lt;template&gt;
  &lt;p&gt; Vue component {{ buildType }} the build step&lt;/p&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: {
    buildType: {
      type: String
    }
  }
}
&lt;/script&gt;
</code></pre>

<p>We updated the HTML template with the interpolated <code>buildType</code>, which will get executed and replaced with the value of the props that will be passed down from the parent component.</p>

<p>We also added a <strong>props</strong> tag in the props option property to listen to the props change and update the template accordingly. Within this props option property, we declared the name of the props, which matches what we have in the <code>&lt;template&gt;</code> tag, and also added the <strong>props type</strong>.</p>

<p>The props type, which can be Strings, Numbers, Arrays, Boolean, or Objects, acts as a rule or check to determine what our component will receive.</p>

<p>In the example above, we added a type of String; we will get an error if we try to pass in any other kind of value like a Boolean or Object.</p>

<h3 id="passing-props-in-vue">Passing Props In Vue</h3>

<p>To wrap this up, we will update the parent file, i.e., the <code>App.vue</code>, and pass the props accordingly.</p>

<pre><code class="language-html">&lt;!-- App.vue --&gt;
&lt;template&gt;
  &lt;div&gt;
    &lt;BuildStep buildType="with"/&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import BuildStep from './BuildStep.vue'

export default {
  components: {
    BuildStep
  }
}
&lt;/script&gt;
</code></pre>

<p>Now, when the <strong>build step</strong> component gets rendered, we will see something like the following:</p>

<pre><code class="language-html">Vue component with the build step
</code></pre>

<p>With props, we needn’t create a new component from scratch to display whether a component has a build step or not. We can again declare the <code>&lt;BuildStep /&gt;</code> component and add the relevant build type.</p>

<pre><code class="language-html">&lt;!-- App..vue --&gt;
&lt;template&gt;
  &lt;div&gt;
    &lt;BuildStep buildType="without"/&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>

<p>Likewise, just as for the build step, when the component gets rendered, we will have the following view:</p>

<pre><code class="language-html">Vue component without the build step
</code></pre>

<h2 id="event-handling-in-vue">Event Handling In Vue</h2>

<p>Vue has many directives, which include the <code>v-on</code>. The <code>v-on</code> is responsible for listening and handling DOM events to act when triggered. The <code>v-on</code> directive can also be written as the <code>@</code> symbol to reduce verbosity.</p>

<pre><code class="language-html">&lt;button @click="checkBuildType"&gt; Check build type &lt;/button&gt;
</code></pre>

<p>The button tag in the above code block has a click event attached to a <code>checkBuildType</code> method. When this button gets clicked, it facilitates executing a function that checks for the build type of the component.</p>

<h3 id="event-modifiers">Event Modifiers</h3>

<p>The <code>v-on</code> directive has several event modifiers that add unique attributes to the <code>v-on</code> event handler. These event modifiers start with a dot and are found right after the event modifier name.</p>

<div class="break-out">

<pre><code class="language-html">&lt;form @submit.prevent="submitData"&gt;
 ...
&lt;!-- This enables a form to be submitted while preventing the page from being reloaded. --&gt;
&lt;/form&gt;
</code></pre>
</div>

<h3 id="key-modifiers">Key Modifiers</h3>

<p>Key modifiers help us listen to keyboard events, such as <code>enter</code>, and <code>page-up</code> on the fly. Key modifiers are bound to the <code>v-on</code> directive like <code>v-on:eventname.keymodifiername</code>, where the <code>eventname</code> could be <code>keyup</code> and the <code>modifiername</code> as <code>enter</code>.</p>

<pre><code class="language-html">&lt;input @keyup.enter="checkInput"&gt;
</code></pre>

<p>The key modifiers also offer flexibility but allow multiple key name chaining.</p>

<pre><code class="language-html">&lt;input @keyup.ctrl.enter="checkInput"&gt;
</code></pre>

<p>Here the key names will listen for both the <code>ctrl</code> and the <code>enter</code> keyboard events before the <code>checkInput</code> method gets called.</p>

<div class="partners__lead-place"></div>

<h2 id="the-v-for-directive">The <code>v-for</code> Directive</h2>

<p>Just as JavaScript provides for iterating through arrays using loops like the <code>for</code> loop, Vue-js also provides a built-in directive known as the <code>v-for</code> that performs the same function.</p>

<p>We can write the <code>v-for</code> syntax as <code>item in items</code> where <strong>items</strong> are the array we are iterating over or as <code>items of items</code> to express the similarity with the JavaScript loop syntax.</p>

<h3 id="list-rendering">List Rendering</h3>

<p>Let us consider rendering the types of component build steps on a page.</p>

<div class="break-out">

<pre><code class="language-html">&lt;template&gt;
  &lt;div&gt;
    &lt;ul&gt;
        &lt;li v-for="steps in buildSteps" :key="steps.id"&gt; {{ steps.step }}&lt;/li&gt;
      &lt;/ul&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
 data() {
   return {
     buildSteps: [
      {
       id: "step 1",
       step:'With the build step',
      },
      {
        id: "step 2",
       step:'Without the build step'
      }
    ]
   }
 }
}
&lt;/script&gt;
</code></pre>
</div> 

<p>In the code block above, the <code>steps</code> array within the <code>data</code> property shows the two types of build steps we have for a component. Within our template, we used the <code>v-for</code> directive to loop through the steps array, the result of which we will render in an unordered list.</p>

<p>We added an optional <code>key</code> argument representing the index of the item we are currently iterating on. But beyond that, the <code>key</code> accepts a unique identifier that enables us to track each item’s node for proper state management.</p>

<h3 id="using-v-for-with-a-component">Using <code>v-for</code> With A Component</h3>

<p>Just like using the <code>v-for</code> to render lists, we can also use it to generate components. We can add the <code>v-for</code> directive to the component like the following:</p>

<pre><code class="language-html">&lt;BuildStep v-for="steps in buildSteps" :key="steps.id"/&gt;
</code></pre>

<p>The above code block will not do much for rendering or passing the <code>step</code> to the component. Instead, we will need to pass the value of the <code>step</code> as props to the component.</p>

<div class="break-out">

<pre><code class="language-html">&lt;BuildStep v-for="steps in buildSteps" :key="steps.id" :buildType="steps.step" /&gt;
</code></pre>
</div>

<p>We do the above to prevent any tight fixation of the <code>v-for</code> to the component.</p>

<p>The most important thing to note in the different usage of the <code>v-for</code> is the automation of a long process. We can move from manually listing out 100 items or components to using the <code>v-for</code> directive and have everything rendered out within the split of a second, as the case may be.</p>

<h3 id="building-a-complex-registration-form-in-vue">Building A Complex Registration Form In Vue</h3>

<p>We will combine everything we have learned about the <code>v-model</code>, Vue components, the Vue props, the <code>v-for</code> directive, and event handling to build a complex form that would help us achieve efficiency, scalability, and time management.</p>

<p>This form will cater to capturing students’ bio-data, which we will develop to facilitate progressive enhancement as business demands increase.</p>

<h4 id="setting-up-the-vue-app">Setting Up The Vue App</h4>

<p>We will be scaffolding our Vue application using the build step. To do this, we will need to ensure we have the following installed:</p>

<ul>
<li><a href="https://nodejs.org/en/">Node.js</a>;</li>
<li><a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> or <a href="https://classic.yarnpkg.com/lang/en/docs/install/#windows-stable">yarn</a>.</li>
</ul>

<p>Now we will proceed to create our Vue application by running the command below:</p>

<pre><code class="language-bash"># npm
npm init vue@latest vue-complex-form
</code></pre>

<p>where <code>vue-complex-form</code> is the name of the Vue application.</p>

<p>After that, we will run the command below at the root of our Vue project:</p>

<pre><code class="language-bash">npm install
</code></pre>

<h4 id="creating-the-json-file-to-host-the-form-data">Creating The JSON File To Host The Form Data</h4>

<p>We aim to create a form where users can fill in their details. While we can manually add all the input fields, we will use a different approach to simplify our codebase. We will achieve this by creating a JSON file called <code>util/bio-data.json</code>. Within each of the JSON objects, we will have the basic info we want each input field to have.</p>

<pre><code class="language-javascript">[
  {
    "id": 1,
    "inputvalue":"  ",
    "formdata": "First Name",
    "type": "text",
    "inputdata": "firstname"
  },
  {
    "id": 2,
    "inputvalue":"  ",
    "formdata": "Last Name",
    "type": "text",
    "inputdata": "lastname"
  },
]
</code></pre>

<p>As seen in the code block above, we created an object with some keys already carrying values:</p>

<ul>
<li><code>id</code> acts as the primary identifier of the individual object;</li>
<li><code>inputvalue</code> will cater to the value passed into the <code>v-model</code>;</li>
<li><code>formdata</code> will handle the input placeholder and the labels name;</li>
<li><code>type</code> denotes the input type, such as email, number, or text;</li>
<li><code>inputdata</code> represents the input <strong>id</strong> and <strong>name</strong>.</li>
</ul>

<p>These keys’ values will be passed in later to our component as props. We can access the complete JSON data here.</p>

<h4 id="creating-the-reusable-component">Creating The Reusable Component</h4>

<p>We will create an input component that will get passed the props from the JSON file we created. This input component will get iterated on using a <code>v-for</code> directive to create numerous instances of the input field at a stretch without having to write it all out manually. To do this, we will create a  <code>components/TheInputTemplate.vue</code> file and add the code below:</p>

<pre><code class="language-html">&lt;template&gt;
  &lt;div&gt;
    &lt;label :for="inputData"&gt;{{ formData }}&lt;/label&gt;
    &lt;input
      :value= "modelValue"
      :type= "type"
      :id= "inputData"
      :name= "inputData"
      :placeholder= "formData"
      @input="$emit('update:modelValue', $event.target.value)"
    &gt;
  &lt;/div&gt;
 &lt;/template&gt;
 
&lt;script&gt;
export default {
  name: 'TheInputTemplate',
  props: {
    modelValue: {
      type: String
    },
    formData: {
      type: String
    },
    type: {
      type: String
    },
    inputData: {
      type: String
    }
  },
  emits: ['update:modelValue']
}
&lt;/script&gt;
&lt;style&gt;
label {
  display: inline-block;
  margin-bottom: 0.5rem;
  text-transform: uppercase;
  color: rgb(61, 59, 59);
  font-weight: 700;
  font-size: 0.8rem;
}
input {
  display: block;
  width: 90%;
  padding: 0.5rem;
  margin: 0 auto 1.5rem auto;
}
&lt;/style&gt;
</code></pre>

<p>In the above code block, we achieved the following:</p>

<ul>
<li>We created a component with an input field.</li>
<li>Within the input field, we matched the values that we will pass in from the JSON file to the respective places of interest in the element.</li>
<li>We also created props of <code>modelValue</code>, <code>formData</code>, <code>type</code>, and <code>inputData</code> that will be registered on the component when exported. These props will be responsible for taking in data from the parent file and passing it down to the <code>TheInputTemplate.vue</code> component.</li>
<li>Bound the <code>modelValue</code> prop to the value of the input value.</li>
<li>Added the <code>update:modelValue</code>, which gets emitted when the <code>input</code> event is triggered.</li>
</ul>

<h4 id="registering-the-input-component">Registering The Input Component</h4>

<p>We will navigate to our <code>App.vue</code> file and import the <code>TheInputTemplate.vue</code> component from where we can proceed to use it.</p>

<pre><code class="language-html">&lt;template&gt;
  &lt;form class="wrapper"&gt;
    &lt;TheInputTemplate/&gt;
  &lt;/form&gt;
&lt;/template&gt;
&lt;script&gt;
import TheInputTemplate from './components/TheInputTemplate.vue'
export default {
  name: 'App',
  components: {
    TheInputTemplate
  }
}
&lt;/script&gt;
&lt;style&gt;
html, body{
  background-color: grey;
  height: 100%;
  min-height: 100vh;
}
.wrapper {
  background-color: white;
  width: 50%;
  border-radius: 3px;
  padding: 2rem  1.5rem;
  margin: 2rem auto;
}
&lt;/style&gt;
</code></pre>

<p>Here we imported the <code>TheInputTemplate.vue</code> component into the <code>App.vue</code> file, registered it in the <code>components</code> option property, and then declared it within our HTML template.</p>

<p>If we run <code>npm run serve</code>, we should have the following view:</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/building-complex-forms-vue/1-ui-view.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="306"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/building-complex-forms-vue/1-ui-view.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/building-complex-forms-vue/1-ui-view.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/building-complex-forms-vue/1-ui-view.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/building-complex-forms-vue/1-ui-view.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/building-complex-forms-vue/1-ui-view.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/building-complex-forms-vue/1-ui-view.png"
			
			sizes="100vw"
			alt="Application interface just after rendering the input component and registering the component in the App.vue file"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Input component after registration. (<a href='https://files.smashing.media/articles/building-complex-forms-vue/1-ui-view.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>At this point, there is not much to see because we are yet to register the props on the component.</p>

<h4 id="passing-input-data">Passing Input Data</h4>

<p>To get the result we are after, we will need to pass the input data and add the props to the component. To do this, we will update our <code>App.vue</code> file:</p>

<div class="break-out">

<pre><code class="language-html">&lt;template&gt;
  &lt;div class="wrapper"&gt;
    &lt;div v-for="bioinfo in biodata" :key="bioinfo.id"&gt;
      &lt;TheInputTemplate v-model="bioinfo.inputvalue":formData= "bioinfo.formdata":type= "bioinfo.type":inputData= "bioinfo.inputdata"/&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;script&gt;
//add imports here
import biodata from "../util/bio-data.json";
export default {
  name: 'App',
 //component goes here
  data: () =&gt; ({
    biodata
  })
}
&lt;/script&gt;
</code></pre>
</div>

<p>From the code block above, we achieved several things:</p>

<ul>
<li>We imported the bio-data JSON file we created into the <code>App.vue</code> file. Then we added the imported variable to the <code>data</code> options of the Vue script.</li>
<li>Looped through the JSON data, which we instantiated in the data options using the Vue <code>v-for</code> directive.</li>
<li>Within the <code>TheInputTemplate.vue</code> component we created, we passed in the suitable data to fill the props option.</li>
</ul>

<p>At this point, our interface should look like the following:</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/building-complex-forms-vue/2-interface-input-fields.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="575"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/building-complex-forms-vue/2-interface-input-fields.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/building-complex-forms-vue/2-interface-input-fields.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/building-complex-forms-vue/2-interface-input-fields.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/building-complex-forms-vue/2-interface-input-fields.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/building-complex-forms-vue/2-interface-input-fields.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/building-complex-forms-vue/2-interface-input-fields.png"
			
			sizes="100vw"
			alt="Interface showing the rendered form after passing the props to the input component"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Application view showing the rendered complex form. (<a href='https://files.smashing.media/articles/building-complex-forms-vue/2-interface-input-fields.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To confirm if our application is working as it should, we will open up our Vue DevTools, or install one from <a href="https://devtools.vuejs.org/">https://devtools.vuejs.org</a> if we do not have it in our browser yet.</p>

<p>When we type in a value in any of the input fields, we can see the value show up in the <code>modelValue</code> within the Vue Devtools dashboard.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/building-complex-forms-vue/3-vue-devtools-dashboard.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="265"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/building-complex-forms-vue/3-vue-devtools-dashboard.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/building-complex-forms-vue/3-vue-devtools-dashboard.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/building-complex-forms-vue/3-vue-devtools-dashboard.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/building-complex-forms-vue/3-vue-devtools-dashboard.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/building-complex-forms-vue/3-vue-devtools-dashboard.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/building-complex-forms-vue/3-vue-devtools-dashboard.png"
			
			sizes="100vw"
			alt="Vue Devtools view showing the modelValue of the input value"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Vue DevTools showing the input value. (<a href='https://files.smashing.media/articles/building-complex-forms-vue/3-vue-devtools-dashboard.png'>Large preview</a>)
    </figcaption>
  
</figure>

<div class="partners__lead-place"></div>

<h2 id="conclusion">Conclusion</h2>

<p>In this article, we explored some core Vue fundamentals like the <code>v-for</code>, <code>v-model</code>, and so on, which we later sewed together to build a complex form. The main goal of this article is to simplify the process of building complex forms while maintaining readability and reusability and reducing development time.</p>

<p>If, in any case, there will be a need to extend the form, all the developer would have to do is populate the JSON files with the needed information, and voila, the form is ready. Also, new Engineers can avoid swimming in lengthy lines of code to get an idea of what is going on in the codebase.</p>

<p><strong>Note</strong>: <em>To explore more about handling events within components to deal with as much complexity as possible, you can check out this article on <a href="https://vuejs.org/guide/components/events.html#usage-with-v-model">using components with v-model</a>.</em></p>

<h3 id="further-reading-on-smashing-magazine">Further Reading on Smashing Magazine</h3>

<ul>
<li>“<a href="https://www.smashingmagazine.com/2022/11/optimizing-vue-app/">Optimizing A Vue App</a>,” Michelle Barker</li>
<li>“<a href="https://www.smashingmagazine.com/2021/07/three-insights-vuejs-accessibility/">Three Insights I Gained While Researching Vue.js Accessibility</a>,” Marcus Herrmann</li>
<li>“<a href="https://www.smashingmagazine.com/2021/07/tools-practices-speed-up-vuejs-development-process/">Tools And Practices To Speed Up The Vue.js Development Process</a>,” Uma Victor</li>
<li>“<a href="https://www.smashingmagazine.com/2023/03/vue-case-study-migrating-headless-cms-system/">Moving From Vue 1 To Vue 2 To Vue 3: A Case Study Of Migrating A Headless CMS System</a>,” Lisi Linhart</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(yk, il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Lisi Linhart</author><title>Moving From Vue 1 To Vue 2 To Vue 3: A Case Study Of Migrating A Headless CMS System</title><link>https://www.smashingmagazine.com/2023/03/vue-case-study-migrating-headless-cms-system/</link><pubDate>Thu, 02 Mar 2023 09:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2023/03/vue-case-study-migrating-headless-cms-system/</guid><description>While migrating large front-end systems is usually a daunting task, it can be helpful to understand the reasons and strategies behind it. In this article, Elisabeth Wieser-Linhart explores its potential benefits and drawbacks and shares what considerations and steps were involved in the process of migrating the front-end interface of Storyblok’s headless content management system.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2023/03/vue-case-study-migrating-headless-cms-system/" />
              <title>Moving From Vue 1 To Vue 2 To Vue 3: A Case Study Of Migrating A Headless CMS System</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Moving From Vue 1 To Vue 2 To Vue 3: A Case Study Of Migrating A Headless CMS System</h1>
                  
                    
                    <address>Lisi Linhart</address>
                  
                  <time datetime="2023-03-02T09:00:00&#43;00:00" class="op-published">2023-03-02T09:00:00+00:00</time>
                  <time datetime="2023-03-02T09:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                <p>This article is sponsored by <b>Storyblok</b></p>
                

<p>One of the greatest challenges in software development is not in creating new functionality but in maintaining and upgrading existing systems. With growing dependencies and complexity, it can be a tedious task to continuously keep everything updated. This becomes even more challenging when upgrading a base technology that the whole system runs on.</p>

<p>In this article, we will discuss how Storyblok solved the challenges of migrating the front-end interface of our headless content management system from Vue 1 to Vue 2 to Vue 3 within six years of growing the startup.</p>

<p>While migrating larger front-end systems can be a daunting task, it can be helpful to understand the reasons and strategies behind it. We’ll delve into the considerations and steps involved in such a migration and explore the potential benefits and drawbacks. With a clear understanding of the process, we can approach the migration with more confidence and ensure a smooth transition for our users and stakeholders.</p>

<h2 id="the-vue-ecosystem-storyblok-s-early-days">The Vue Ecosystem &amp; Storyblok’s Early Days</h2>

<p>The Vue.js framework’s first large pre-beta release happened in late 2016, and Storyblok began work on a full prototype built on top of Vue in late 2015. At the time, Vue was still a relatively new framework, and other more established options like React were available. Despite this, Storyblok decided to take a chance on Vue and built their own prototype on top of it. This turned out to be a good decision, as the prototype worked well, and up to today, Vue is kept as the underlying framework for the front-end interface.</p>

<p>Over the years, Storyblok has played a key role in the growth and development of Vue, participating in forums, conferences, and meetups, sponsoring certain projects, as well as contributing to the Vue ecosystem through open-source projects and other initiatives. As Storyblok grew together with the Vue community over the years, Vue started upgrading its framework, and Storyblok began growing out of its prototype to become a fully-fledged product. This is where our migration story starts.</p>

<h2 id="ground-up-migration-vs-soft-migration">Ground-up Migration vs. Soft Migration</h2>

<p>There were two main points in time when Storyblok was facing large migration challenges. The first one was when the upgrade from Vue 1 to Vue 2 happened. This went hand in hand with the update from Storyblok Version 1 to Storyblok Version 2. The decision was to completely rebuild the system from scratch. This is what we’ll call <strong>Ground-up migration</strong>. The second large migration happened when going from Vue 2 to Vue 3, where the front-end interface did not change. The existing codebase was updated in the background without visual changes for the user, and this is what we’ll call <strong>Soft migration</strong>.</p>

<p>Ground-up migration allows for <strong>greater flexibility and control over the design and architecture</strong> of the new system, but it can be a time-consuming and resource-intensive process. For Storyblok, it allowed the development of an <a href="https://blok.ink/">open-source Blok Ink design system</a> as the core of the new system. This design system could then simultaneously be used by our customers to build their own extensions.</p>

<p>Soft migration, on the other hand, can be <strong>quicker and more cost-effective</strong>, but it’s strongly limited and influenced by the current design and architecture of the existing system. Upgrading large codebases and all their dependencies can take months to achieve, and the time for this can be hard to find in a growing business. When thinking about customers, soft migration tends to be easier because the user doesn’t have to relearn a whole new interface, and no large marketing and communication resources need to be allocated towards this kind of migration.</p>

<p>How were these migration decisions made from a business perspective? After Storyblok was launched as a product in 2017 with Vue 1, it was continuously improved and extended with new features. In 2019, the team received its first seed investment, which allowed them to hire more developers to work on Storyblok. In 2020, work began on Storyblok V2 with Vue 2, with around five developers starting on the project for the first time. Instead of updating the old codebase from Vue 1 to Vue 2, the team decided to start with a completely new codebase. This gave the new team two main benefits:</p>

<ol>
<li>The developers were fully involved in creating the architecture of the new system;</li>
<li>They learned how the system worked by rebuilding it.</li>
</ol>

<p>In the core, the decision to make a ground-up migration was correct because the flexibility of that migration allowed the team to build a <em>better, newer, and more stable</em> version of the prototype while also understanding how it works. The main drawbacks of the ground-up migration were the cost of time and resources and getting the buy-in from the customers to switch from the old to the new version.</p>

<p>As Storyblok continued to evolve and improve, the codebase needed to be upgraded from Vue 2 to Vue 3. Migrating to the new version involved updating large amounts of code and ensuring that everything continued to work as expected. In this migration, we had to invest a lot of resources in retesting the interface, as well as allocating time for the developers to learn and understand the changes in Vue 3. This can be especially challenging when working with a large team, as there may be different codebases and business needs to consider.</p>

<p>Ultimately, the decision between these two approaches will depend on the specific needs and circumstances of the migration, including the following:</p>

<ul>
<li>Resources and expertise available,</li>
<li>Complexity and size of the system,</li>
<li>Desired level of control and flexibility over the design and architecture of the new system.</li>
</ul>

<p>It’s important to have a <em>clear</em> plan in place to guide the migration process and ensure a smooth transition, so in the next chapter, we will look into what that plan looked like for us.</p>

<h2 id="strategies-for-large-migrations">Strategies For Large Migrations</h2>

<p>These five factors are essential to consider when planning a migration:</p>

<table class="tablesaw break-out">
    <tbody>
        <tr>
            <td><strong>Time</strong></td>
            <td>Creating a timeline for the migration</td>
        </tr>
        <tr>
            <td><strong>Functionality</strong></td>
            <td>Identify and prioritize critical parts of the system to migrate</td>
        </tr>
        <tr>
            <td><strong>Resources</strong></td>
            <td>Use automation and tools to support the migration</td>
        </tr>
    <tr>
            <td><strong>Acceptance</strong></td>
            <td>Engage and communicate with users</td>
        </tr>
    <tr>
            <td><strong>Risk</strong></td>
            <td>Monitor and evaluate the migration process</td>
        </tr>
    </tbody>
</table>

<h3 id="creating-a-timeline">Creating A Timeline</h3>

<p>One of the main challenges of migration is getting the buy-in from the organization, clients, and teams. Since migrations aren’t adding any new functionality, it can be hard to convince a team and the product owners of the importance of migration. However,</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aIn%20the%20long%20term,%20migrations%20are%20necessary,%20and%20the%20longer%20you%20put%20them%20off,%20the%20harder%20they%20will%20become.%0a&url=https://smashingmagazine.com%2f2023%2f03%2fvue-case-study-migrating-headless-cms-system%2f">
      
In the long term, migrations are necessary, and the longer you put them off, the harder they will become.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>Secondly, migrations tend to take more than a few weeks, so it’s a harder and more tedious process that has to be planned with all the developers and stakeholders. Here is what our timeline looked like:</p>

<h4 id="going-from-vue-1-to-vue-2">Going from Vue 1 to Vue 2</h4>

<p>This process took us around two years from start to finish:</p>

<ul>
<li>Before Mid&ndash;2020: Develop new features in the old version.</li>
<li>2020 June: Identify and develop ‘core’ components in a new open-source design system.</li>
<li>2020 November: Create a new Vue 2 project.</li>
<li>2020 Nov &mdash; 2021 August: Redevelop all parts of the old app in the new app with the new design system.</li>
<li>2021 August: Beta Release of parts of the new application (e.g., our Visual Editor).</li>
<li>2021 August &mdash; 2022 August: Add all the missing functionality from the old app, add new features, and improve overall UX through customer feedback.</li>
<li>2022 August: Official release of the new app.</li>
<li>During that time: onboard 20+ developers, designers, QAs, and product owners.</li>
</ul>

<h4 id="going-from-vue-2-to-vue-3">Going from Vue 2 to Vue 3</h4>

<p>This process took us around eight months:</p>

<ul>
<li>2022 July &mdash; 2022 November: Start migrating the Design System to Vue 3.</li>
<li>2022 November &mdash; 2023 January: remove all the ‘old’ breaking functionality and update or replace old dependencies that depend on Vue 2.</li>
<li>2022 December: Several meetings to explain what changed in Vue 3 with developers and stakeholders and how the transition will happen.</li>
<li>2023 January: Introduce the Vue 3 codebase, switch developers to the new codebase, and thoroughly test and start developing in Vue 3.</li>
<li>2023 February: Launch the Vue 3 version into production.</li>
</ul>

<p>To create a timeline, you need to identify all the parts of the migration first. This can be done in an excel sheet or a planning tool like Jira. Here is an example of a simple sheet that could be used for creating a rough timeline. It can be useful to split different areas to separate sheets to divide them into the teams they belong to.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/1-migration-timeline-excel-sheet.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="370"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/1-migration-timeline-excel-sheet.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/1-migration-timeline-excel-sheet.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/1-migration-timeline-excel-sheet.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/1-migration-timeline-excel-sheet.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/1-migration-timeline-excel-sheet.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/1-migration-timeline-excel-sheet.png"
			
			sizes="100vw"
			alt="An excel sheet with the migration timeline"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/1-migration-timeline-excel-sheet.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="identifying-and-prioritizing-functionality-to-migrate">Identifying And Prioritizing Functionality to Migrate</h3>

<p>In order to manage the cost and resources required for the migration, it is essential to identify and prioritize the <strong>critical features and functionality of the system</strong> that need to be migrated. For us, it meant starting bit by bit with more important functionality. When we built the new version of Storyblok, we started with our most important core feature, the “Visual Editor,” and built an entirely new version of it using our Design System. In any migration, you should ask yourself these questions to find out what the priorities are:</p>

<ul>
<li>Is the goal to create a completely new version of something?</li>
<li>What parts does the system have? Can some of them be migrated separately?</li>
<li>Which parts of the system are most important to the customer?</li>
<li>Who knows the system well and can help with the migration?</li>
<li>Does every developer need to work on the migration, or can a few ‘experts’ be selected to focus on this task?</li>
<li>Can I estimate how long the migration will take? If not, can I break down the parts of the system more to find out how long smaller parts of the system can be migrated?</li>
</ul>

<p>If you have multiple sub-parts that can be migrated separately, it’s easier to split them to different people to work on the migration. Another big decision is <em>who</em> is working on the migration. For our Vue 1 to Vue 2 migration, all our developers worked on creating the new functionality, but on the Vue 2 to Vue 3, it was one expert (the person who is writing this article) who did most of the migration and then it was handed over to the teams to finish and retest the whole application to see if everything was still working as it should.</p>

<p>At the same time, I organized some training for the developers to dive deeper into Vue 3 and the breaking changes in the system.</p>

<blockquote>The core of the migration strategy always depends on the knowledge of the system as well as the importance of the features to be migrated.</blockquote>

<p>More important features will be migrated and worked on first, and less important things can be kept to be improved in the end.</p>

<h3 id="use-automation-and-tools-to-support-the-migration">Use Automation And Tools to Support The Migration</h3>

<p>Since a migration process is always very time-consuming, it pays off to invest in some automation of tedious tasks to find out all the problems of the migration. For our migration from Vue 2 to Vue 3, <a href="https://v3-migration.vuejs.org/migration-build.html">the migration build</a> and its linting were very helpful in finding all the potential problem areas in the app that needed to be changed. We also worked with hand-written scripts that iterate through all Vue files in the project (we have over 600 of them) to automatically add all missing <strong><a href="https://v3-migration.vuejs.org/breaking-changes/emits-option.html">emit notations</a></strong>, replace event names, that updated in Vue 3 and update common logic changes in the unit tests, like adding the <strong><a href="https://test-utils.vuejs.org/migration/#mocks-and-stubs-are-now-in-global">global notation</a></strong>.</p>

<p>These scripts were a large timesaver since we didn’t have to touch hundreds of files by hand, so investing time in writing such scripts can really pay off. In the core, utilizing regex to find and replace large logic chunks can help, but for the last final stretch, you will still spend hours fixing some things manually.</p>

<p>In the final part, all the unit and end-to-end tests we already had, helped to find some of the potential problems in the new version of our app. For unit testing, we used Jest with the built-in <a href="https://test-utils.vuejs.org/">Test Utils</a> as well as the <a href="https://testing-library.com/docs/vue-testing-library/intro">Vue Testing Library</a>, for the e2e tests, we’re using <a href="https://www.cypress.io/">Cypress</a> with different plugins.</p>

<h3 id="engage-and-communicate-with-users">Engage And Communicate With Users</h3>

<p>If you create a new version of something, it’s essential to make sure that the experience is not getting worse for the customer. This can involve providing training and support to help users understand and use the new version, as well as collecting feedback and suggestions from users to help improve the new system. For us, this was a very important learning during the ground-up migration from Vue 1 to Vue 2 because we continuously collected feedback from the customer while rebuilding the app at the same time. This helped us to ensure that what we were building was what the customers wanted.</p>

<p>Another way was to have the beta version accessible way ahead of the new version being finished. In the beginning, we only made the Partner Portal available, then the Visual Editor, then the Content Editing, and lastly, the missing parts of the app. By gradually introducing the new parts, we could collect feedback during the development time and adjust things where the customer experience was not perfect yet.</p>

<p>In any scenario, it will be important to ask yourself these questions in a ground-up migration:</p>

<ul>
<li>How can we collect feedback from our customers early and often to ensure what we’re building is working?</li>
<li>How can we communicate with the existing customers to make them aware of using the new version?</li>
<li>What are the communication channels for the update? Can we leverage the update with the marketing team to make users more excited about it?</li>
<li>Who do we have internally that handles communication, and who are the stakeholders who should be updated regularly on the changes?</li>
</ul>

<p>For us, this meant building ‘feedback buttons’ into the interface to collect feedback from the users starting to use the new version. It pointed to a form where the users could rate specific parts but also give open feedback. This feedback was then handed back to the design team to evaluate and forward if it was valid feedback. Further, we added channels in our Discord to hear directly from developers using the system and introduced ‘tour’ functionalities that showed users where all the buttons and features are located and what they do. Finally, we added buttons to the old app to seamlessly switch between the old and new versions. We did various campaigns and videos on social media to hype our community about using the new version.</p>

<p>In all cases, <strong>it’s crucial to find out who the core stakeholders and communicators of the migration are</strong>. You can find that out with a simple impact/influence matrix. This matrix documents who is impacted by the migration and who should have how much influence over the migration. This can indicate who you should be in close contact with and who might only need to be communicated with occasionally.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/3-communication-stakeholders.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="578"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/3-communication-stakeholders.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/3-communication-stakeholders.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/3-communication-stakeholders.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/3-communication-stakeholders.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/3-communication-stakeholders.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/3-communication-stakeholders.png"
			
			sizes="100vw"
			alt="Matrix with communication stakeholders"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/3-communication-stakeholders.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="monitor-and-evaluate-the-migration-process">Monitor And Evaluate The Migration Process</h3>

<p>Since a migration process will always take a few months or even years to accomplish, it’s essential to monitor the process and make sure that after a few months, it’s still going in the right direction.</p>

<p>Here are some questions that can help you figure out if you’re on the right track:</p>

<ul>
<li>Have all the areas and functionalities that need to be migrated been defined? How are we tracking the process of each area?</li>
<li>Are all stakeholders, including users and other teams, being effectively communicated with and their concerns addressed?</li>
<li>Are there unexpected issues that have arisen during the migration that require you to adjust the budget and time frame of the migration?</li>
<li>Is the new version being properly tested and validated before being put into production?</li>
<li>Are the final results of the migration meeting the expectations of the users and stakeholders?</li>
</ul>

<p>During the different migration phases, we hit several roadblocks. One of them was the <strong>customer’s expectations</strong> of having the exact same functionality from the old app in the new app. Initially, we wanted to change some of the UX and deprecate some features that weren’t satisfying to us. But over time, we noticed that it was really important to customers to be close to what they already knew from the older version, so over time, we moved many parts to be closer to how the customer expected it to be from before.</p>

<p>The second big roadblock in the migration from Vue 2 to Vue 3 was the <strong>migration of all our testing environments</strong>. Initially, we didn’t expect to have to put so much effort into updating the unit tests, but updating them was, at times, more time-consuming than the app itself. The testing in the “soft migration” had to be very extensive, and it took us more than three weeks to find and fix all the issues. During this time, we depended heavily on the skills of our QA engineers to help us figure out anything that might not work anymore.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/2-migration-testing-excel-sheet.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="370"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/2-migration-testing-excel-sheet.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/2-migration-testing-excel-sheet.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/2-migration-testing-excel-sheet.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/2-migration-testing-excel-sheet.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/2-migration-testing-excel-sheet.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/2-migration-testing-excel-sheet.png"
			
			sizes="100vw"
			alt="An excel sheet with the migration testing"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/vue-case-study-migrating-headless-cms-system/2-migration-testing-excel-sheet.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The final step in the migration from Vue 2 to Vue 3 was to put the new version of the app into production. Since we had two versions of our app, one in Vue 2 and one in Vue 3, which looked essentially the same, we decided to do a blue/green deployment. This means that we transferred a subset of the user traffic to the new app while the majority of the users were still using the stable old app. We then tested this in production for a week with only a small subset of our users.</p>

<p>By slowly introducing the new version to just a percentage of the users, we could find more potential issues without disrupting all of our users. The essential part here was to have direct communication with our Sales and Support teams, who are in direct contact with important clients.</p>

<h2 id="lessons-learnt">Lessons Learnt</h2>

<h3 id="migration-to-vue-2">Migration to Vue 2</h3>

<p>The core lesson when we completely rebuilt Storyblok in Vue 2 was that migrating a large system can be a significant challenge for the development team that consists of new developers who are not familiar with the system yet. By handing the power over to the developers, they could be directly involved in forming the architecture, quality, and direction of the product. In any migration or significant change, the onboarding and training of developers will be an essential part of making the migration successful.</p>

<p>Another big lesson was the involvement of the existing users to improve the quality of the newer version we were building. By slowly introducing the new design and features in different areas of the system, the customers had the opportunity to get used to the new version gradually. With this gradual change, they could give feedback and report any issues or problems they encountered.</p>

<p>Overall, customers have a number of expectations when it comes to software, including:</p>

<ul>
<li>Reliability and stability,</li>
<li>Ease of use,</li>
<li>Regular updates and improvements,</li>
<li>Support and assistance,</li>
<li>Security and privacy.</li>
</ul>

<p>Migrating a large system can impact these expectations, and it&rsquo;s important to carefully consider the potential impacts on customers and take steps to ensure a smooth transition for them.</p>

<h3 id="migration-to-vue-3">Migration to Vue 3</h3>

<p>As we got more teams and more customers, it kept getting more critical to keep our production version of the app as stable as possible, so testing was an important part of making sure the quality was monitored and bugs were eliminated. All that time we had invested in unit and e2e testing in the Vue 2 version helped us to find problems and bugs during the migration process to Vue 3.</p>

<p>We found that it was essential to test the migrated system extensively to ensure that it was functioning correctly and meeting all of the required specifications. By carefully planning and executing our testing process, we were able to identify and fix the majority of the issues before the migrated system was released to production.</p>

<p>During the Vue 3 migration, we also saw the advantages of having a ‘separate’ part of the system, our design system, that could be migrated first. This allowed us to learn and study the migration guide there first and then move to the more complex migration of the app itself.</p>

<p>Lastly, a big thing we learned was that <strong>communication is essential</strong>. We started creating internal communication channels on Slack to keep marketing and other teams up to date on all the functionality changes and new features. Certain people could then weigh in on the ways new features were built.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Migrating Storyblok from Vue 1 to Vue 2 to Vue 3 was a significant challenge for the whole organization. A well-formed migration plan should outline the steps to be taken, the areas and functionalities that need to be migrated and in what way (rebuild or create a new version), the people to be involved, and the expected timeline.</p>

<p>For us, some key takeaways were the importance of involving the development team in forming the architecture and direction of the product, as well as <strong>onboarding and training</strong> them properly. It was also essential to communicate effectively with stakeholders, including users and other teams, to address their concerns and ensure their expectations were met.</p>

<p>Testing plays a crucial role in ensuring the migrated system functions correctly, and the better your QA engineers, the more smoothly the migration will go. Gradually introducing the new version to a subset of users before a full release can help identify any potential issues without disrupting all users. Another approach is gradually introducing new parts of the system one by one. We made use of both of them.</p>

<p>If you’re planning a large system migration, starting with a <strong>well-defined plan</strong> is important. Make sure to consider budget and time frame, build in time for testing and validation, continuously monitor the progress of the migration and adjust the plan as necessary.</p>

<p>It’s also important to involve existing users in the migration process to gather feedback and identify any potential issues. Make sure your migration is <em>actually</em> improving the experience for the users or developers. <strong>Internal communication</strong> is key in this process, so ensure that all stakeholders are effectively communicated with throughout the migration to make sure everyone is on board. Consider gradually migrating different parts of the system to help manage complexity or introducing a new system only to a subset of users first.</p>

<p>And lastly, work with your team. Migrating really takes a lot of hands and time, so the better your teams can work together, the smoother the transition will go.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(yk, il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Michelle Barker</author><title>Optimizing A Vue App</title><link>https://www.smashingmagazine.com/2022/11/optimizing-vue-app/</link><pubDate>Tue, 22 Nov 2022 16:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2022/11/optimizing-vue-app/</guid><description>Prioritizing performance when building our web apps improves the user experience and helps ensure they can be used by as many people as possible. In this article, Michelle Barker will walk you through some of the front-end optimization tips to keep our Vue apps as efficient as possible.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2022/11/optimizing-vue-app/" />
              <title>Optimizing A Vue App</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Optimizing A Vue App</h1>
                  
                    
                    <address>Michelle Barker</address>
                  
                  <time datetime="2022-11-22T16:00:00&#43;00:00" class="op-published">2022-11-22T16:00:00+00:00</time>
                  <time datetime="2022-11-22T16:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>Single Page Applications (SPAs) <em>can</em> provide a rich, interactive user experience when dealing with real-time, dynamic data. But they can also be heavy, bloated, and perform poorly. In this article, we’ll walk through some of the front-end optimization tips to keep our Vue apps relatively lean and only ship the JS we need when it’s needed.</p>

<p><strong>Note</strong>: <em>Some familiarity with <a href="https://vuejs.org/">Vue</a> and the Composition API is assumed, but there will hopefully be some useful takeaways regardless of your framework choice.</em></p>

<p>As a front-end developer at <a href="https://ada-mode.com/">Ada Mode</a>, my job involves building Windscope, a web app for wind farm operators to manage and maintain their fleet of turbines. Due to the need to receive data in real time and the high level of interactivity required, an SPA architecture was chosen for the project. Our web app is dependent on some heavy JS libraries, but we want to provide the best experience for the end user by fetching data and rendering as quickly and efficiently as possible.</p>

<h2 id="choosing-a-framework">Choosing A Framework</h2>

<p>Our JS framework of choice is Vue, partly chosen as it’s the framework I’m most familiar with. Previously Vue had a smaller overall bundle size compared to React. However, since recent React updates, the balance appears to have shifted in React’s favor. That doesn’t necessarily matter, as we’ll look at how to only import what we need in the course of this article. Both frameworks have excellent documentation and a large developer ecosystem, which was another consideration. <a href="https://svelte.dev">Svelte</a> is another possible choice, but it would have required a steeper learning curve due to unfamiliarity, and being newer, it has a less developed ecosystem.</p>

<p>As an example to demonstrate the various optimizations, I’ve built a simple Vue app that fetches data from an API and renders some charts using <a href="https://d3js.org/">D3.js</a>.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/84dc6ee7-ee6a-4f66-a434-54278637d3c8/1-optimizing-vue-app.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="439"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/84dc6ee7-ee6a-4f66-a434-54278637d3c8/1-optimizing-vue-app.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/84dc6ee7-ee6a-4f66-a434-54278637d3c8/1-optimizing-vue-app.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/84dc6ee7-ee6a-4f66-a434-54278637d3c8/1-optimizing-vue-app.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/84dc6ee7-ee6a-4f66-a434-54278637d3c8/1-optimizing-vue-app.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/84dc6ee7-ee6a-4f66-a434-54278637d3c8/1-optimizing-vue-app.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/84dc6ee7-ee6a-4f66-a434-54278637d3c8/1-optimizing-vue-app.png"
			
			sizes="100vw"
			alt="Visualization of the Vue app with some charts"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/84dc6ee7-ee6a-4f66-a434-54278637d3c8/1-optimizing-vue-app.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><strong>Note</strong>: <em>Please refer to the <a href="https://github.com/mbarker84/vue-app-example">example GitHub repository</a> for the full code.</em></p>

<p>We’re using <a href="https://parceljs.org">Parcel</a>, a minimal-config build tool, to bundle our app, but all of the optimizations we’ll cover here are applicable to whichever bundler you choose.</p>

<h2 id="tree-shaking-compression-and-minification-with-build-tools">Tree Shaking, Compression, And Minification With Build Tools</h2>

<p>It’s good practice to only ship the code you need, and right out of the box, Parcel removes unused Javascript code during the build process (tree shaking). It also minifies the result and can be configured to compress the output with Gzip or Brotli.</p>

<p>As well as minification, Parcel also employs <a href="https://parceljs.org/features/scope-hoisting/">scope hoisting</a> as part of its production process, which can help make minification even more efficient. An in-depth guide to scope hoisting is outside of the scope (see what I did there?) of this article. Still, if we run Parcel’s build process on our example app with the <code>--no-optimize</code> and <code>--no-scope-hoist</code> flags, we can see the resulting bundle is 510kB &mdash; around <strong>5 times higher</strong> than the optimized and minified version. So, whichever bundler you’re using, it’s fair to say you’ll probably want to make sure it’s carrying out as many optimizations as possible.</p>

<p>But the work doesn’t end here. Even if we’re shipping a smaller bundle overall, it still takes time for the browser to parse and compile our JS, which can contribute to a slower user experience. This article on <a href="https://calibreapp.com/blog/bundle-size-optimization">Bundle Size Optimization</a> by Calibre explains how large JS bundles affect performance metrics.</p>

<p>Let’s look at what else we can do to reduce the amount of work the browser has to do.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Roll up your sleeves and <strong>boost your UX skills</strong>! Meet <strong><a data-instant href="https://smart-interface-design-patterns.com/">Smart Interface Design Patterns</a></strong>&nbsp;🍣, a 10h video library by Vitaly Friedman. <strong>100s of real-life examples</strong> and live UX training. <a href="https://www.youtube.com/watch?v=3mwZztmGgbE">Free preview</a>.</p>
<a data-instant href="https://smart-interface-design-patterns.com/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://smart-interface-design-patterns.com/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3155f571-450d-42f9-81b4-494aa9b52841/video-course-smart-interface-design-patterns-vitaly-friedman.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8c98e7f9-8e62-4c43-b833-fc6bf9fea0a9/video-course-smart-interface-design-patterns-vitaly-friedman.jpg"
    alt="Feature Panel"
    width="690"
    height="790"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="vue-composition-api">Vue Composition API</h2>

<p>Vue 3 introduced the <a href="https://vuejs.org/guide/extras/composition-api-faq">Composition API</a>, a new set of APIs for authoring components as an alternative to the Options API. By exclusively using the Composition API, we can import only the Vue functions that we need instead of the whole package. It also enables us to write more reusable code using <a href="https://vuejs.org/guide/reusability/composables.html#what-is-a-composable">composables</a>. Code written using the Composition API lends itself better to minification, and the whole app is more susceptible to tree-shaking.</p>

<p><strong>Note</strong>: <em>You can still use the Composition API if you’re using an older version of Vue: it was backported to Vue 2.7, and there is <a href="https://github.com/vuejs/composition-api">an official plugin</a> for older versions.</em></p>

<h2 id="importing-dependencies">Importing Dependencies</h2>

<p>A key goal was to reduce the size of the initial JS bundle downloaded by the client. Windscope makes extensive use of D3 for data visualization, a large library and wide-ranging in scope. However, Windscope only needs part of it (there are entire modules in the D3 library that we don’t need at all). If we examine the entire D3 package on <a href="https://bundlephobia.com">Bundlephobia</a>, we can see that our app uses less than half of the available modules and perhaps not even all of the functions within those modules.</p>

<p>One of the easiest ways to keep our bundle size as small as possible is only to import the modules we need.</p>

<p>Let’s take D3’s <code>selectAll</code> function. Instead of using a default import, we can just import the function we need from the <code>d3-selection</code> module:</p>

<pre><code class="language-javascript">// Previous:
import &#42; as d3 from 'd3'

// Instead:
import { selectAll } from 'd3-selection'
</code></pre>

<h2 id="code-splitting-with-dynamic-imports">Code Splitting With Dynamic Imports</h2>

<p>There are certain packages that are used in a bunch of places throughout Windscope, such as the AWS Amplify authentication library, specifically the <code>Auth</code> method. This is a large dependency that contributes heavily to our JS bundle size. Rather than import the module statically at the top of the file, <a href="https://javascript.info/modules-dynamic-imports"><strong>dynamic imports</strong></a> allow us to import the module exactly where we need it in our code.</p>

<p>Instead of:</p>

<pre><code class="language-javascript">import { Auth } from '@aws-amplify/auth'

const user = Auth.currentAuthenticatedUser()
</code></pre>

<p>We can import the module when we want to use it:</p>

<pre><code class="language-javascript">import('@aws-amplify/auth').then(({ Auth }) =&gt; {
    const user = Auth.currentAuthenticatedUser()
})
</code></pre>

<p>This means that the module will be split out into a separate JS bundle (or “chunk”), which will only be downloaded by the browser if and when it is needed. Additionally, the browser can cache these dependencies, which may change less frequently than the code for the rest of our app.</p>

<h2 id="lazy-loading-routes-with-vue-router">Lazy Loading Routes With Vue Router</h2>

<p>Our app uses <a href="https://router.vuejs.org/">Vue Router</a> for navigation. Similarly to dynamic imports, we can lazyload our route components, so they will only be imported (along with their associated dependencies) when a user navigates to that route.</p>

<p>In our <code>index/router.js</code> file:</p>

<pre><code class="language-javascript">// Previously:
import Home from "../routes/Home.vue";
import About = "../routes/About.vue";

// Lazyload the route components instead:
const Home = () =&gt; import("../routes/Home.vue");
const About = () =&gt; import("../routes/About.vue");

const routes = [
  {
    name: "home",
    path: "/",
    component: Home,
  },
  {
    name: "about",
    path: "/about",
    component: About,
  },
];
</code></pre>

<p>The code for the ‘About’ route will only be loaded when the user clicks the ‘About’ link and navigates to the route.</p>

<h2 id="async-components">Async Components</h2>

<p>In addition to lazyloading each route, we can also lazyload individual components using Vue’s <code>defineAsyncComponent</code> method.</p>

<div class="break-out">

<pre><code class="language-javascript">const KPIComponent = defineAsyncComponent(() =&gt; import('../components/KPI.vue))
</code></pre>
</div>

<p>This means the code for the KPI component will be dynamically imported, as we saw in the router example. We can also provide some components to display while it’s in a loading or error state (useful if we’re loading a particularly large file).</p>

<pre><code class="language-javascript">const KPIComponent = defineAsyncComponent({
  loader: () =&gt; import('../components/KPI.vue),
  loadingComponent: Loader,
  errorComponent: Error,
  delay: 200,
  timeout: 5000,
});
</code></pre>

<div class="partners__lead-place"></div>

<h2 id="splitting-api-requests">Splitting API Requests</h2>

<p>Our application is primarily concerned with data visualization and relies heavily on fetching large amounts of data from the server. Some of these requests can be quite slow, as the server has to perform a number of computations on the data. In our initial prototype, we made a single request to the REST API per route. Unfortunately, we found this resulted in users having to wait a long time &mdash; sometimes up to 10 seconds, watching a loading spinner before the app successfully received the data and could begin rendering the visualizations.</p>

<p>We made the decision to split the API into several endpoints and make a request for each widget. While this could increase the response time <strong>overall</strong>, it means the app should become usable much quicker, as users will see parts of the page rendered while they’re still waiting for others. Additionally, any error that might occur will be localized while the rest of the page remains usable.</p>

<p>You can see the difference illustrated here:</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/773762352"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>In the example on the right, the user can interact with some components while others are still requesting data. The page on the left has to wait for a large data response before it can be rendered and become interactive.</figcaption>
	
</figure>

<h3 id="conditionally-load-components">Conditionally Load Components</h3>

<p>Now we can combine this with async components to only load a component when we’ve received a successful response from the server. Here we’re fetching the data, then importing the component when our <code>fetch</code> function returns successfully:</p>

<div class="break-out">

<pre><code class="language-javascript">&lt;template&gt;
  &lt;div&gt;
    &lt;component :is="KPIComponent" :data="data"&gt;&lt;/component&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import {
  defineComponent,
  ref,
  defineAsyncComponent,
} from "vue";
import Loader from "./Loader";
import Error from "./Error";

export default defineComponent({
    components: { Loader, Error },

    setup() {
        const data = ref(null);

        const loadComponent = () =&gt; {
          return fetch('https://api.npoint.io/ec46e59905dc0011b7f4')
            .then((response) =&gt; response.json())
            .then((response) =&gt; (data.value = response))
            .then(() =&gt; import("../components/KPI.vue") // Import the component
            .catch((e) =&gt; console.error(e));
        };

        const KPIComponent = defineAsyncComponent({
          loader: loadComponent,
          loadingComponent: Loader,
          errorComponent: Error,
          delay: 200,
          timeout: 5000,
        });

        return { data, KPIComponent };
    }
}
</code></pre>
</div>

<p>To handle this process for every component, we created a <a href="https://www.freecodecamp.org/news/higher-order-components-the-ultimate-guide-b453a68bb851/">higher order component</a> called <code>WidgetLoader</code>, which you can see in the repository.</p>

<p>This pattern can be extended to any place in the app where a component is rendered upon user interaction. For example, in Windscope, we load a map component (and its dependencies) only when the user clicks on the ‘Map’ tab. This is known as <a href="https://www.patterns.dev/posts/import-on-interaction/"><strong>Import on interaction</strong></a>.</p>

<h3 id="css">CSS</h3>

<p>If you run the example code, you will see that clicking the ‘Locations’ navigation link loads the map component. As well as dynamically importing the JS module, importing the dependency within the component’s <code>&lt;style&gt;</code> block will lazyload the CSS too:</p>

<pre><code class="language-css">// In MapView.vue
&lt;style&gt;
@import "../../node_modules/leaflet/dist/leaflet.css";

.map-wrapper {
  aspect-ratio: 16 / 9;
}
&lt;/style&gt;
</code></pre>

<h3 id="refining-the-loading-state">Refining The Loading State</h3>

<p>At this point, we have our API requests running in parallel, with components being rendered at different times. One thing we might notice is the page appears janky, as the layout will be shifting around quite a bit.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/773765559"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>A quick way to make things feel a bit smoother for users is to set an aspect ratio on the widget that roughly corresponds to the rendered component so the user doesn’t see quite as big a layout shift. We could pass in a prop for this to account for different components, with a default value to fall back to.</p>

<div class="break-out">

<pre><code class="language-javascript">// WidgetLoader.vue
&lt;template&gt;
  &lt;div class="widget" :style="{ 'aspect-ratio': loading ? aspectRatio : '' }"&gt;
    &lt;component :is="AsyncComponent" :data="data"&gt;&lt;/component&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent, ref, onBeforeMount, onBeforeUnmount } from "vue";
import Loader from "./Loader";
import Error from "./Error";

export default defineComponent({
  components: { Loader, Error },

  props: {
    aspectRatio: {
      type: String,
      default: "5 / 3", // define a default value
    },
    url: String,
    importFunction: Function,
  },

  setup(props) {
      const data = ref(null);
      const loading = ref(true);

        const loadComponent = () =&gt; {
          return fetch(url)
            .then((response) =&gt; response.json())
            .then((response) =&gt; (data.value = response))
            .then(importFunction
            .catch((e) =&gt; console.error(e))
            .finally(() =&gt; (loading.value = false)); // Set the loading state to false
        };

    /&#42; ...Rest of the component code &#42;/

    return { data, aspectRatio, loading };
  },
});
&lt;/script&gt;
</code></pre>
</div>

<h2 id="aborting-api-requests">Aborting API Requests</h2>

<p>On a page with a large number of API requests, what should happen if the user navigates away before all the requests have been completed? We probably don’t want those requests to continue running in the background, slowing down the user experience.</p>

<p>We can use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController">AbortController</a> interface, which enables us to abort API requests as desired.</p>

<p>In our <code>setup</code> function, we create a new controller and pass its signal into our fetch request parameters:</p>

<pre><code class="language-javascript">setup(props) {
    const controller = new AbortController();

    const loadComponent = () =&gt; {
      return fetch(url, { signal: controller.signal })
        .then((response) =&gt; response.json())
        .then((response) =&gt; (data.value = response))
        .then(importFunction)
        .catch((e) =&gt; console.error(e))
        .finally(() =&gt; (loading.value = false));
        };
}
</code></pre>

<p>Then we abort the request before the component is unmounted, using Vue’s <code>onBeforeUnmount</code> function:</p>

<pre><code class="language-javascript">onBeforeUnmount(() =&gt; controller.abort());
</code></pre>

<p>If you run the project and navigate to another page before the requests have been completed, you should see errors logged in the console stating that the requests have been aborted.</p>

<div class="partners__lead-place"></div>

<h2 id="stale-while-revalidate">Stale While Revalidate</h2>

<p>So far, we’ve done a pretty good of optimizing our app. But when a user navigates to the second view and then back to the previous one, all the components remount and are returned to their loading state, and we have to wait for the request responses all over again.</p>

<p><a href="https://web.dev/stale-while-revalidate/">Stale-while-revalidate</a> is an HTTP cache invalidation strategy where the browser determines whether to serve a response from the cache if that content is still fresh or “revalidate” and serve from the network if the response is stale.</p>

<p>In addition to applying cache-control headers to our HTTP response (out of the scope of this article, but read <a href="https://web.dev/stale-while-revalidate/">this article from Web.dev</a> for more detail), we can apply a similar strategy to our Vue component state, using the <a href="https://docs-swrv.netlify.app/">SWRV</a> library.</p>

<p>First, we must import the composable from the SWRV library:</p>

<pre><code class="language-javascript">import useSWRV from "swrv";
</code></pre>

<p>Then we can use it in our <code>setup</code> function. We’ll rename our <code>loadComponent</code> function to <code>fetchData</code>, as it will only deal with data fetching. We’ll no longer import our component in this function, as we’ll take care of that separately.</p>

<p>We’ll pass this into the <code>useSWRV</code> function call as the second argument. We only need to do this if we need a custom function for fetching data (maybe we need to update some other pieces of state). As we’re using an Abort Controller, we’ll do this; otherwise, the second argument can be omitted, and SWRV will use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">Fetch API</a>:</p>

<pre><code class="language-javascript">// In setup()
const { url, importFunction } = props;

const controller = new AbortController();

const fetchData = () =&gt; {
  return fetch(url, { signal: controller.signal })
    .then((response) =&gt; response.json())
    .then((response) =&gt; (data.value = response))
    .catch((e) =&gt; (error.value = e));
};

const { data, isValidating, error } = useSWRV(url, fetchData);
</code></pre>

<p>Then we’ll remove the <code>loadingComponent</code> and <code>errorComponent</code> options from our async component definition, as we’ll use SWRV to handle the error and loading states.</p>

<pre><code class="language-javascript">// In setup()
const AsyncComponent = defineAsyncComponent({
  loader: importFunction,
  delay: 200,
  timeout: 5000,
});
</code></pre>

<p>This means we’ll need to include the <code>Loader</code> and <code>Error</code> components in our template and show and hide them depending on the state. The <code>isValidating</code> return value tells us whether there is a request or revalidation happening.</p>

<div class="break-out">

<pre><code class="language-javascript">&lt;template&gt;
  &lt;div&gt;
    &lt;Loader v-if="isValidating && !data"&gt;&lt;/Loader&gt;
    &lt;Error v-else-if="error" :errorMessage="error.message"&gt;&lt;/Error&gt;
    &lt;component :is="AsyncComponent" :data="data" v-else&gt;&lt;/component&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import {
  defineComponent,
  defineAsyncComponent,
} from "vue";
import useSWRV from "swrv";

export default defineComponent({
  components: {
    Error,
    Loader,
  },

  props: {
    url: String,
    importFunction: Function,
  },

  setup(props) {
    const { url, importFunction } = props;

    const controller = new AbortController();

    const fetchData = () =&gt; {
      return fetch(url, { signal: controller.signal })
        .then((response) =&gt; response.json())
        .then((response) =&gt; (data.value = response))
        .catch((e) =&gt; (error.value = e));
    };

    const { data, isValidating, error } = useSWRV(url, fetchData);

    const AsyncComponent = defineAsyncComponent({
      loader: importFunction,
      delay: 200,
      timeout: 5000,
    });

    onBeforeUnmount(() =&gt; controller.abort());

    return {
      AsyncComponent,
      isValidating,
      data,
      error,
    };
  },
});
&lt;/script&gt;
</code></pre>
</div>

<p>We could refactor this into its own composable, making our code a bit cleaner and enabling us to use it anywhere.</p>

<div class="break-out">

<pre><code class="language-javascript">// composables/lazyFetch.js
import { onBeforeUnmount } from "vue";
import useSWRV from "swrv";

export function useLazyFetch(url) {
  const controller = new AbortController();

  const fetchData = () =&gt; {
    return fetch(url, { signal: controller.signal })
      .then((response) =&gt; response.json())
      .then((response) =&gt; (data.value = response))
      .catch((e) =&gt; (error.value = e));
  };

  const { data, isValidating, error } = useSWRV(url, fetchData);

  onBeforeUnmount(() =&gt; controller.abort());

  return {
    isValidating,
    data,
    error,
  };
}
</code></pre>
</div>

<div class="break-out">

<pre><code class="language-javascript">// WidgetLoader.vue
&lt;script&gt;
import { defineComponent, defineAsyncComponent, computed } from "vue";
import Loader from "./Loader";
import Error from "./Error";
import { useLazyFetch } from "../composables/lazyFetch";

export default defineComponent({
  components: {
    Error,
    Loader,
  },

  props: {
    aspectRatio: {
      type: String,
      default: "5 / 3",
    },
    url: String,
    importFunction: Function,
  },

  setup(props) {
    const { aspectRatio, url, importFunction } = props;
    const { data, isValidating, error } = useLazyFetch(url);

    const AsyncComponent = defineAsyncComponent({
      loader: importFunction,
      delay: 200,
      timeout: 5000,
    });

    return {
      aspectRatio,
      AsyncComponent,
      isValidating,
      data,
      error,
    };
  },
});
&lt;/script&gt;
</code></pre>
</div>

<h3 id="updating-indicator">Updating Indicator</h3>

<p>It might be useful if we could show an indicator to the user while our request is revalidating so that they know the app is checking for new data. In the example, I’ve added a small loading indicator in the corner of the component, which will only be shown if there is already data, but the component is checking for updates. I’ve also added a simple fade-in transition on the component (using Vue’s built-in <code>Transition</code> component), so there is not such an abrupt jump when the component is rendered.</p>

<div class="break-out">

<pre><code class="language-javascript">&lt;template&gt;
  &lt;div
    class="widget"
    :style="{ 'aspect-ratio': isValidating && !data ? aspectRatio : '' }"
  &gt;
    &lt;Loader v-if="isValidating && !data"&gt;&lt;/Loader&gt;
    &lt;Error v-else-if="error" :errorMessage="error.message"&gt;&lt;/Error&gt;
    &lt;Transition&gt;
        &lt;component :is="AsyncComponent" :data="data" v-else&gt;&lt;/component&gt;
    &lt;/Transition&gt;

    &lt;!--Indicator if data is updating--&gt;
    &lt;Loader
      v-if="isValidating && data"
      text=""
    &gt;&lt;/Loader&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>
</div>

<h2 id="conclusion">Conclusion</h2>

<p>Prioritizing performance when building our web apps improves the user experience and helps ensure they can be used by as many people as possible. We’ve successfully used the above techniques at Ada Mode to make our applications faster. I hope this article has provided some pointers on how to make your app as efficient as possible &mdash; whether you choose to implement them in full or in part.</p>

<p>SPAs can work well, but they can also be a performance bottleneck. So, let’s try to build them better.</p>

<h3 id="further-reading-on-smashing-magazine">Further Reading on Smashing Magazine</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2021/07/refactoring-css-introduction-part1/">Refactoring CSS (Part 1&ndash;3)</a></li>
<li><a href="https://www.smashingmagazine.com/2022/09/data-loading-patterns-improve-frontend-performance/">Five Data-Loading Patterns To Boost Web Performance</a></li>
<li><a href="https://www.smashingmagazine.com/2022/05/performance-game-changer-back-forward-cache/">Performance Game Changer: Browser Back/Forward Cache</a></li>
<li><a href="https://www.smashingmagazine.com/2022/02/reducing-web-carbon-footprint-optimizing-social-media-embeds/">Reducing The Web’s Carbon Footprint: Optimizing Social Media Embeds</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(vf, yk, il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Joseph Zimmerman</author><title>How To Make A Drag-and-Drop File Uploader With Vue.js 3</title><link>https://www.smashingmagazine.com/2022/03/drag-drop-file-uploader-vuejs-3/</link><pubDate>Fri, 18 Mar 2022 10:30:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2022/03/drag-drop-file-uploader-vuejs-3/</guid><description>Building on a previous article on &lt;a href="https://www.smashingmagazine.com/2018/01/drag-drop-file-uploader-vanilla-js/">How to Build a Drag-and-Drop File Uploader&lt;/a>, we’ll be adding some new features, but more importantly (maybe), we’ll be learning how to build it in Vue 3 and learn some best practices for Vue along the waxy.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2022/03/drag-drop-file-uploader-vuejs-3/" />
              <title>How To Make A Drag-and-Drop File Uploader With Vue.js 3</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>How To Make A Drag-and-Drop File Uploader With Vue.js 3</h1>
                  
                    
                    <address>Joseph Zimmerman</address>
                  
                  <time datetime="2022-03-18T10:30:00&#43;00:00" class="op-published">2022-03-18T10:30:00+00:00</time>
                  <time datetime="2022-03-18T10:30:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>What’s different about the file uploader we’re building in this article versus the previous one? The previous drag-and-drop file uploader was built with Vanilla JS and really focused on how to make file uploading and drag-and-drop file selection work, so its feature set was limited. It uploaded the files immediately after you chose them with a simple progress bar and an image thumbnail preview. You can see all of this at <a href="https://codepen.io/joezimjs/pen/yPWQbd">this demo</a>.</p>

<p>In addition to using Vue, we’ll be changing the features up: after an image is added, it will not upload immediately. Instead, a thumbnail preview will show up. There will be a button on the top right of the thumbnail that will remove the file from the list in case you didn’t mean to select an image or change your mind about uploading it.</p>

<p>You’ll then click on the “Upload” button to send the image data to the server and each image will display its upload status. To top it all off, I crafted some snazzy styles (I’m no designer, though, so don’t judge too harshly). We won’t be digging into those styles in this tutorial, but they’ll be available for you to copy or sift through yourself in the <a href="https://github.com/joezimjs/vue-dd-uploader">GitHub Repository</a> — though, if you’re going to copy them, make sure you set up your project to be able to use Stylus styles (or you can set it up to use Sass and change <code>lang</code> to <code>scss</code> for the style blocks and it will work that way). You can also see what we’re building today on <a href="https://vue-dd-uploader.pages.dev/">the demo page</a>.</p>

<p><strong>Note</strong>: <em>I will assume that readers have strong JavaScript knowledge and a good grasp of the Vue features and APIs, especially Vue 3’s composition API, but not necessarily the best ways to use them. This article is to learn how to create a drag-and-drop uploader in the context of a Vue app while discussing good patterns and practices and will not go deep into how to use Vue itself.</em></p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="/printed-books/image-optimization/">Image Optimization</a></strong>, Addy Osmani’s new practical guide to optimizing and delivering <strong>high-quality images</strong> on the web. Everything in one single <strong>528-pages</strong> book.</p>
<a data-instant href="https://www.smashingmagazine.com/printed-books/image-optimization/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://www.smashingmagazine.com/printed-books/image-optimization/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c669cf1-c6ef-4c87-9901-018b04f7871f/image-optimization-shop-cover-opt.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/87fd0cfa-692e-459c-b2f3-15209a1f6aa7/image-optimization-shop-cover-opt.png"
    alt="Feature Panel"
    width="480"
    height="697"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="setup">Setup</h2>

<p>There are a lot of ways to set up a Vue project: <a href="https://cli.vuejs.org/guide/">Vue CLI</a>, <a href="https://vitejs.dev/guide/#scaffolding-your-first-vite-project">Vite</a>, <a href="https://v3.nuxtjs.org/getting-started/installation">Nuxt</a>, and <a href="https://quasar.dev/start/pick-quasar-flavour">Quasar</a> all have their own project scaffolding tools, and I’m sure there are more. I’m not all that familiar with most of them, and I’m not going to prescribe any one tool as of right for this project, so I recommend reading the documentation for whichever you choose to figure out how to set up the way we need it for this little project.</p>

<p>We need to be set up with Vue 3 with the <a href="https://v3.vuejs.org/api/sfc-script-setup.html">script setup</a> syntax, and if you’re snatching my styles from the <a href="https://github.com/joezimjs/vue-dd-uploader">Github repo</a>, you’ll need to make sure you’re set up to have your Vue styles compiled from Stylus (or you can set it up to use Sass and change <code>lang</code> to “scss” for the style blocks and it will work that way).</p>

<h2 id="drop-zone">Drop Zone</h2>

<p>Now that we have the project set up, let’s dive into the code. We’ll start with a component that handles the drag-and-drop functionality. This will be a simple wrapper <code>div</code> element with a bunch of event listeners and emitters for the most part. This sort of element is a great candidate for a reusable component (despite it only being used once in this particular project): it has a very specific job to do and that job is generic enough to be used in a lot of different ways/places without the need of a ton of customization options or complexity.</p>

<p>This is one of those things good developers are always keeping an eye out for. Cramming a ton of functionality into a single component would be a bad idea for this project or any other because then 1) it can’t be reused if you find a similar situation later and 2) it’s more difficult to sort through the code and figure out how each piece relates to each other. So, we’re going to do what we can to follow this principle and it starts here with the <code>DropZone</code> component. We’ll start with a simple version of the component and then spruce it up a bit to help you grok what’s going on a bit easier, so let’s create a <code>DropZone.vue</code> file in the <code>src/components</code> folder:</p>

<pre><code class="language-html">&lt;template&gt;
    &lt;div @drop.prevent="onDrop"&gt;
        &lt;slot&gt;&lt;/slot&gt;
    &lt;/div&gt;
&lt;/template&gt;

&lt;script setup&gt;
import { onMounted, onUnmounted } from 'vue'
const emit = defineEmits(['files-dropped'])

function onDrop(e) {
    emit('files-dropped', [...e.dataTransfer.files])
}

function preventDefaults(e) {
    e.preventDefault()
}

const events = ['dragenter', 'dragover', 'dragleave', 'drop']

onMounted(() =&gt; {
    events.forEach((eventName) =&gt; {
        document.body.addEventListener(eventName, preventDefaults)
    })
})

onUnmounted(() =&gt; {
    events.forEach((eventName) =&gt; {
        document.body.removeEventListener(eventName, preventDefaults)
    })
})
&lt;/script&gt;</code></pre>

<p>First, looking at the template, you’ll see a <code>div</code> with a <code>drop</code> event handler (with a <code>prevent</code> modifier to prevent default actions) calling a function that we’ll get to in a moment. Inside that <code>div</code> is a <code>slot</code>, so we can reuse this component with custom content inside it. Then we get to the JavaScript code, which is inside a <code>script</code> tag with the <code>setup</code> attribute.</p>

<p>Inside the script, we define an event that we’ll emit called ‘files-dropped’ that other components can use to do something with the files that get dropped here. Then we define the function <code>onDrop</code> to handle the drop event. Right now, all it does is emit the event we just defined and add an array of the files that were just dropped as the payload. Note, we’re using a trick with the spread operator to convert the list of files from the <code>FileList</code> that <code>e.dataTransfer.files</code> gives us to an array of <code>File</code>s so all the array methods can be called on it by the part of the system that takes the files.</p>

<p>Finally, we come to the place where we handle the other drag/drop events that happen on the body, preventing the default behavior during the drag and drop (namely that it’ll open one of the files in the browser. We create a function that simply calls <code>preventDefault</code> on the event object. Then, in the <code>onMounted</code> lifecycle hook we iterate over the list of events and prevent default behavior for that even on the document body. In the <code>onUnmounted</code> hook, we remove those listeners.</p>

<h3 id="active-state">Active State</h3>

<p>So, what extra functionality can we add? The one thing I decided to add was some state indicating whether the drop zone was “active”, meaning that a file is currently hovering over the drop zone. That’s simple enough; create a <code>ref</code> called <code>active</code>, set it to true on the events when the files are dragged over the drop zone and false when they leave the zone or are dropped.</p>

<p>We’ll also want to expose this state to the components using <code>DropZone</code>, so we’ll turn our <code>slot</code> into a scoped slot and expose that state there. Instead of the scoped slot (or in addition to it for added flexibility), we could emit an event to inform the outside of the value of <code>active</code> as it changes. The advantage of this is that the entire component that is using <code>DropZone</code> can have access to the state, rather than it being limited to the components/elements within the slot in the template. We’re going to stick with the scoped slot for this article though.</p>

<p>Finally, for good measure, we’ll add a <code>data-active</code> attribute that reflects <code>active</code>&rsquo;s value so we can key off it for styling. You could also use a class if you prefer, but I tend to like data attributes for state modifiers.</p>

<p>Let’s write it out:</p>

<div class="break-out">

<pre><code class="language-html">&lt;template&gt;
    &lt;!-- add `data-active` and the event listeners --&gt;
    &lt;div :data-active="active" @dragenter.prevent="setActive" @dragover.prevent="setActive" @dragleave.prevent="setInactive" @drop.prevent="onDrop"&gt;
        &lt;!-- share state with the scoped slot --&gt;
        &lt;slot :dropZoneActive="active"&gt;&lt;/slot&gt;
    &lt;/div&gt;
&lt;/template&gt;

&lt;script setup&gt;
// make sure to import `ref` from Vue
import { ref, onMounted, onUnmounted } from 'vue'
const emit = defineEmits(['files-dropped'])

// Create `active` state and manage it with functions
let active = ref(false)

function setActive() {
    active.value = true
}
function setInactive() {
    active.value = false
}

function onDrop(e) {
    setInactive() // add this line too
    emit('files-dropped', [...e.dataTransfer.files])
}

// ... nothing changed below this
&lt;/script&gt;
</code></pre>

</div>

<p>I threw some comments in the code to note where the changes were, so I won’t dive too deep into it, but I have some notes. We’re using the <code>prevent</code> modifiers on all the event listeners again to make sure that default behavior doesn’t activate. Also, you’ll notice that the <code>setActive</code> and <code>setInactive</code> functions seem like a bit of overkill since you could just set <code>active</code> directly, and you could make that argument for sure, but just wait a bit; there will be another change that truly justifies the creation of functions.</p>

<p>You see, there’s an issue with what we’ve done. As you can see in the video below, using this code for the drop zone means that it can flicker between active and inactive states while you drag something around inside the drop zone.</p>


<figure class="video-embed-container break-out">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="nqAxR3GcMI8"
      
			videotitle="Flickery Drag Interaction"
		></lite-youtube>
	</div>
	
		<figcaption>Flickery Drag Interaction</figcaption>
	
</figure>

<p>Why is it doing that? When you drag something over a child element, it will “enter” that element and “leave” the drop zone, which causes it to go inactive. The <code>dragenter</code> event will bubble up to the drop zone, but it happens before the <code>dragleave</code> event, so that doesn’t help. Then a <code>dragover</code> event will fire again on the drop zone which will flip it back to active but not before flickering to the inactive state.</p>

<p>To fix this, we’ll add a short timeout to the <code>setInactive</code> function to prevent it from going inactive immediately. Then <code>setActive</code> will clear that timeout so that if it is called before we actually set it as inactive, it won’t actually become inactive. Let’s make those changes:</p>

<pre><code class="language-javascript">// Nothing changed above

let active = ref(false)
let inActiveTimeout = null // add a variable to hold the timeout key

function setActive() {
    active.value = true
    clearTimeout(inActiveTimeout) // clear the timeout
}
function setInactive() {
    // wrap it in a `setTimeout`
    inActiveTimeout = setTimeout(() =&gt; {
        active.value = false
    }, 50)
}

// Nothing below this changes</code></pre>

<p>You’ll note a timeout of 50 milliseconds. Why this number? Because I’ve tested several different timeouts and this feels the best.</p>

<p>I know that’s subjective but hear me out. I’ve tested much smaller timeouts and 15ms was about as low as I went where I never saw a flicker, but who knows how that’ll work on other hardware? It has too small a margin of error in my mind. You also probably don’t want to go over 100ms because that can cause perceived lag when a user intentionally does something that <em>should</em> cause it to go inactive. In the end, I settled somewhere in the middle that is long enough to pretty much guarantee there won’t be any flickering on any hardware and there should be no perceived lag.</p>

<p>That’s all we need for the <code>DropZone</code> component, so let’s move on to the next piece of the puzzle: a file list manager.</p>

<h2 id="file-list-manager">File List Manager</h2>

<p>I guess the first thing that needs to be done is an explanation of what I mean by the file list manager. This will be a composition function that returns several methods for managing the state of the files the user is attempting to upload. This could also be implemented as a <a href="https://vuex.vuejs.org/">Vuex</a>/<a href="https://pinia.vuejs.org/">Pinia</a>/<a href="https://awesome-vue.js.org/components-and-libraries/utilities.html#state-management">alternative</a> store as well, but to keep things simple and prevent needing to install a dependency if we don’t need to, it makes a lot of sense to keep it as a composition function, especially since the data isn’t likely to be needed widely across the application, which is where the stores are the most useful.</p>

<p>You could also just build the functionality directly into the component that will be using our <code>DropZone</code> component, but this functionality seems like something that could very easily be reused; pulling it out of the component makes the component easier to understand the intent of what is going on (assuming good function and variable names) without needing to wade through the entire implementation.</p>

<p>Now that we’ve made it clear this is going to be a composition function and why, here’s what the file list manager will do:</p>

<ol>
<li>Keep a list of files that have been selected by the user;</li>
<li>Prevent duplicate files;</li>
<li>Allow us to remove files from the list;</li>
<li>Augment the files with useful metadata: an ID, a URL that can be used to show a preview of the file, and the file’s upload status.</li>
</ol>

<p>So, let’s build it in <code>src/compositions/file-list.js</code>:</p>

<pre><code class="language-javascript">import { ref } from 'vue'

export default function () {
    const files = ref([])

    function addFiles(newFiles) {
        let newUploadableFiles = [...newFiles]
            .map((file) =&gt; new UploadableFile(file))
            .filter((file) =&gt; !fileExists(file.id))
        files.value = files.value.concat(newUploadableFiles)
    }

    function fileExists(otherId) {
        return files.value.some(({ id }) =&gt; id === otherId)
    }

    function removeFile(file) {
        const index = files.value.indexOf(file)

        if (index &gt; -1) files.value.splice(index, 1)
    }

    return { files, addFiles, removeFile }
}

class UploadableFile {
    constructor(file) {
        this.file = file
        this.id = `${file.name}-${file.size}-${file.lastModified}-${file.type}`
        this.url = URL.createObjectURL(file)
        this.status = null
    }
}</code></pre>

<p>We’re exporting a function by default that returns the file list (as a <code>ref</code>) and a couple of methods that are used to add and remove files from the list. It would be nice to make the file list returned as read-only to force you to use the methods for manipulating the list, which you can do pretty easily using the <code>readonly</code> function imported from Vue, but that would cause issues with the uploader that we’ll build later.</p>

<p>Note that <code>files</code> is scoped to the composition function and set inside it, so each time you call the function, you’ll receive a new file list. If you want to share the state across multiple components/calls, then you’ll need to pull that declaration out of the function so it’s scoped and set once in the module, but in our case we’re only using it once, so it doesn’t really matter, and I was working under the thought that each instance of the file list would be used by a separate uploader and any state can be passed down to child components rather than shared via the composition function.</p>

<p>The most complex piece of this file list manager is adding new files to the list. First, we’re making sure that if a <code>FileList</code> object was passed instead of an array of <code>File</code> objects, then we convert it to an array (as we did in the <code>DropZone</code> when we emitted the files. This means we could probably skip that transformation, but better safe than sorry). Then we convert the file to an <code>UploadableFile</code>, which is a class we’re defining that wraps the file and gives us a few extra properties. We’re generating an <code>id</code> based on several aspects of the file so we can detect duplicates, a <code>blob://</code> URL of the image so we can show preview thumbnails and a status for tracking uploads.</p>

<p>Now that we have the IDs on the files, we filter out any files that already exist in the file list before concatenating them to the end of the file list.</p>

<h3 id="possible-improvements">Possible Improvements</h3>

<p>While this file list manager works well for what it does, there are a number of upgrades that can be done. For one thing, instead of wrapping the file in a new class and then having to call <code>.file</code> on it to access the original file object, we could wrap the file in a proxy that specifies our new properties, but then will forward any other property requests on to the original object, so it is more seamless.</p>

<p>As an alternative to wrapping each file in an <code>UploadableFile</code>, we could have provided utility functions that could return the ID or URL given a file, but that’s slightly less convenient and would mean that you’re potentially calculating these properties multiple times (for each render, and so on), but that shouldn’t really matter unless you’re dealing with people dropping thousands of images at once, in which case you can try memorizing it.</p>

<p>As for the status, that isn’t pulled straight from the <code>File</code>, so a simple utility function like the others wouldn’t be possible, but you could store the status of each file with the uploader (we’ll be building that later) rather than directly with the files. This might be a better way of handling it in a large app so we don’t end up filling the <code>UploadableFile</code> class with a bunch of properties that just facilitate a single area of the app and are useless elsewhere.</p>

<p><strong>Note</strong>: <em>For our purposes, having the properties available directly on our file object is by far the most convenient, but it can definitely be argued that it isn’t the most appropriate.</em></p>

<p>Another possible improvement is allowing you to specify a filter so that it only allows certain file types to be added to the list. This would also require <code>addFiles</code> to return errors when some files don’t match the filter in order to let the user know they made a mistake. This is definitely something that should be done in production-ready applications.</p>

<div class="partners__lead-place"></div>

<h2 id="better-together">Better Together</h2>

<p>We’re far from a finished product, but let’s put the pieces we have together to verify everything is working so far. We’re going to be editing the <code>/src/App.vue</code> file, to put these pieces in, but you can add them to whatever page/section component you want. If you’re putting it inside an alternate component, though, ignore anything (like an ID of “app”) that would only be seen on the main app component.</p>

<div class="break-out">

<pre><code class="language-html">&lt;template&gt;
    &lt;div id="app"&gt;
        &lt;DropZone class="drop-area" @files-dropped="addFiles" #default="{ dropZoneActive }"&gt;
            &lt;div v-if="dropZoneActive"&gt;
                &lt;div&gt;Drop Them&lt;/div&gt;
            &lt;/div&gt;
            &lt;div v-else&gt;
                &lt;div&gt;Drag Your Files Here&lt;/div&gt;
            &lt;/div&gt;
        &lt;/DropZone&gt;
    &lt;/div&gt;
&lt;/template&gt;

&lt;script setup&gt;
import useFileList from './compositions/file-list'
import DropZone from './components/DropZone.vue'

const { files, addFiles, removeFile } = useFileList()
&lt;/script&gt;</code></pre>

</div>

<p>If you start with the <code>script</code> section, you’ll see we’re not doing a whole lot. We’re importing the two files we just finished writing and we’re initializing the file list. Note, we’re not using <code>files</code> or <code>removeFile</code> yet, but we will later, so I’m just keeping them there for now. Sorry if ESLint is complaining about unused variables. We’ll want <code>files</code> at the very least so we can see if it’s working later.</p>

<p>Moving on to the template, you can see we’re using the <code>DropZone</code> component right away. We’re giving it a class so we can style it, passing the <code>addFiles</code> function for the “files-dropped” event handler, and grabbing the scoped slot variable so our content can be dynamic based on whether or not the drop zone is active. Then, inside the drop zone’s slot, we create a <code>div</code> showing a message to drag files over if it’s inactive and a message to drop them when it is active.</p>

<p>Now, you’ll probably want some styles to at least make the drop zone larger and easier to find. I won’t be pasting any here, but you can find the styles I used for <a href="https://github.com/joezimjs/vue-dd-uploader/blob/main/src/App.vue"><code>App.vue</code> in the repo</a>.</p>

<p>Now, before we can test the current state of the app, we’ll need the beta version of Vue DevTools installed in our browser (stable version doesn’t support Vue 3 quite yet). You can get <a href="https://chrome.google.com/webstore/detail/vuejs-devtools/ljjemllljcmogpfapbkkighbhhppjdbg">Vue DevTools from Chrome web store</a> for most Chromium-based browsers or <a href="https://github.com/vuejs/vue-devtools/releases/download/v6.0.0-beta.8/vuejs_devtools_beta-6.0.0.8-an+fx.xpi">download Vue DevTools here for Firefox</a>.</p>

<p>After you’ve installed that, run your app with <code>npm run serve</code> (Vue CLI), <code>npm run dev</code> (Vite), or whatever script you use in your app, then open it in your browser via the URL given in the command line. Open up the Vue DevTools, then drag and drop some images onto the drop zone. If it worked, you should see an array of however many files you added when you view the component we just wrote (see screenshot below).</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f5a0917c-20b3-475c-9a0f-05a31a61cb94/1-drag-and-drop-file-uploader-vuejs-3.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="273"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f5a0917c-20b3-475c-9a0f-05a31a61cb94/1-drag-and-drop-file-uploader-vuejs-3.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f5a0917c-20b3-475c-9a0f-05a31a61cb94/1-drag-and-drop-file-uploader-vuejs-3.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f5a0917c-20b3-475c-9a0f-05a31a61cb94/1-drag-and-drop-file-uploader-vuejs-3.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f5a0917c-20b3-475c-9a0f-05a31a61cb94/1-drag-and-drop-file-uploader-vuejs-3.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f5a0917c-20b3-475c-9a0f-05a31a61cb94/1-drag-and-drop-file-uploader-vuejs-3.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f5a0917c-20b3-475c-9a0f-05a31a61cb94/1-drag-and-drop-file-uploader-vuejs-3.png"
			
			sizes="100vw"
			alt="A screenshot with Vue Devtools showing the files we added"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Vue DevTools showing the files we added. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f5a0917c-20b3-475c-9a0f-05a31a61cb94/1-drag-and-drop-file-uploader-vuejs-3.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Nice! Now Let’s make this a bit more accessible for users who can’t (or don’t want to) drag and drop, by adding a hidden file input (that becomes visible when focused via keyboard for those that need it, assuming you’re using my styles) and wrapping a big label around everything to allow us to use it despite its invisibility. Finally, we’ll need to add an event listener to the file input so that when a user selects a file, we can add it to our file list.</p>

<p>Let’s start with the changes to the <code>script</code> section. We’re just going to add a function to the end of it:</p>

<pre><code class="language-javascript">function onInputChange(e) {
    addFiles(e.target.files)
    e.target.value = null
}</code></pre>

<p>This function handles the “change” event fired from the input and adds the files from the input to the file list. Note the last line in the function resetting the value of the input. If a user adds a file via the input, decides to remove it from our file list, then changes their mind and decides to use the input to add that file again, then the file input will not fire the “change” event because the file input has not changed. By resetting the value like this, we ensure the event will always be fired.</p>

<p>Now, let’s make our changes to the template. Change all of the code inside the <code>DropZone</code> slot to the following:</p>

<pre><code class="language-javascript">&lt;label for="file-input"&gt;
    &lt;span v-if="dropZoneActive"&gt;
        &lt;span&gt;Drop Them Here&lt;/span&gt;
        &lt;span class="smaller"&gt;to add them&lt;/span&gt;
    &lt;/span&gt;
    &lt;span v-else&gt;
        &lt;span&gt;Drag Your Files Here&lt;/span&gt;
        &lt;span class="smaller"&gt;
            or &lt;strong&gt;&lt;em&gt;click here&lt;/em&gt;&lt;/strong&gt; to select files
        &lt;/span&gt;
    &lt;/span&gt;

    &lt;input type="file" id="file-input" multiple @change="onInputChange" /&gt;
&lt;/label&gt;</code></pre>

<p>We wrap the entire thing in a label that is linked to the file input, then we add our dynamic messages back in, though I’ve added a bit more messages to inform users they can click to select files. I also added a bit for the “drop them” message so that they have the same number of lines of text so the drop zone won’t change size when active. Finally, we add the file input, set the <code>multiple</code> attribute to allow users to select multiple files at a time, then wire up the “change” event listener to the function we just wrote.</p>

<p>Run the app again, if you stopped it, we should see the same result in the Vue DevTools whether we drag and drop files or click the box to use the file selector.</p>

<h2 id="previewing-selected-images">Previewing Selected Images</h2>

<p>Great, but users aren’t going to be using Vue DevTools to see if the files they dropped are actually added, so let’s start showing the users those files. We’ll start just by editing <code>App.vue</code> (or whatever component file you added the <code>DropZone</code> to) and showing a simple text list with the file names.</p>

<p>Let’s add the following bit of code to the template immediately following the <code>label</code> we just added in the previous step:</p>

<pre><code class="language-javascript">&lt;ul v-show="files.length"&gt;
    &lt;li v-for="file of files" :key="file.id"&gt;{{ file.file.name }}&lt;/li&gt;
&lt;/ul&gt;</code></pre>

<p>Now, with the app running, if you add some files to the list, you should see a bulleted list of the file names. If you copied my styles, it might look a bit odd, but that’s alright because we’re changing it soon. Make note that thanks to adding the file’s ID in the file list manager, we now have a key in the loop. The only thing that annoys me personally is that since we wrapped the files, we need to write <code>file.file</code> to access the original file object to get its name. In the end, though, it’s a small sacrifice to make.</p>

<p>Now, let’s start showing the images instead of just listing their names, but it’s time to move this functionality out of this main component. We certainly could, keep putting the file preview functionality here, but there are two good reasons to pull it out:</p>

<ol>
<li>The functionality is potentially reusable in other cases.</li>
<li>As this functionality expands, separating it out prevents the main component from getting too bloated.</li>
</ol>

<p>So, let’s create <code>/src/FilePreview.vue</code> to put this functionality in and we’ll start with just showing the image in a wrapper.</p>

<pre><code class="language-html">&lt;template&gt;
    &lt;component :is="tag" class="file-preview"&gt;
        &lt;img :src="file.url" :alt="file.file.name" :title="file.file.name" /&gt;
    &lt;/component&gt;
&lt;/template&gt;

&lt;script setup&gt;
defineProps({
    file: { type: Object, required: true },
    tag: { type: String, default: 'li' },
})
&lt;/script&gt;</code></pre>

<p>Once again, the styles aren’t included here, but you can find them <a href="https://github.com/joezimjs/vue-dd-uploader/blob/main/src/components/FilePreview.vue">on GitHub</a>. First thing to note about the code we have, though, is that we’re wrapping this in a <code>component</code> tag and setting what type of tag it is with a <code>tag</code> prop. This can be a good way to make a component more generic and reusable. We’re currently using this inside an unordered list, so <code>li</code> is the obvious choice, but if we want to use this component somewhere else at some point, it might not be in a list, so we would want a different tag.</p>

<p>For the image, we’re using the URL created by the file list manager, and we’re using the file name as the alt text and as the <code>title</code> attribute so we get that free functionality of users being able to hover over the image and see the file name as a tooltip. Of course, you can always create your own file preview where the file name is written out where it’s always visible for the user. There’s certainly a lot of freedom in how this can be handled.</p>

<p>Moving on to the JavaScript, we see props defined so we can pass in the file that we’re previewing and a tag name to customize the wrapper in order to make this usable in more situations.</p>

<p>Of course, if you try to run this, it doesn’t seem to do anything because we currently aren’t using the <code>FilePreview</code> components. Let’s remedy that now. In the template, replace the current list with this:</p>

<pre><code class="language-javascript">&lt;ul class="image-list" v-show="files.length"&gt;
    &lt;FilePreview v-for="file of files" :key="file.id" :file="file" tag="li" /&gt;
&lt;/ul&gt;</code></pre>

<p>Also, we need to import our new component in the <code>script</code> section:</p>

<pre><code class="language-javascript">import  FilePreview  from  './components/FilePreview.vue'</code></pre>

<p>Now if you run this, you’ll see some nice thumbnails of each image you drop or select.</p>

<h3 id="remove-files-from-the-list">Remove Files From the List</h3>

<p>Let’s augment this with the ability to remove a file from the list. We’ll add a button with an “X” in the corner of the image that people can click/tap on to remove the image. To do this, we’ll need to add 2 lines of code to <code>FilePreview.vue</code>. In the template, just above the <code>img</code> tag add the following:</p>

<pre><code class="language-javascript">&lt;button @click="$emit('remove', file)" class="close-icon" aria-label="Remove"&gt;&times;&lt;/button&gt;</code></pre>

<p>Then add this line somewhere in the <code>script</code> section:</p>

<pre><code class="language-javascript">defineEmits(['remove'])</code></pre>

<p>Now, clicking that button will fire a <code>remove</code> event, passing the file along as the payload. Now we need to head back to the main app component to handle that event. All we need to do is to add the event listener to the <code>FilePreview</code> tag:</p>

<pre><code class="language-javascript">&lt;FilePreview  v-for="file  of  files" :key="file.id" :file="file"  tag="li" @remove="removeFile" /&gt;</code></pre>

<p>Thanks to <code>removeFile</code> already being defined by the file list manager and taking the same arguments that we’re passing from the event, we’re done in seconds. Now if you run the app and select some images, you can click on the little “X” and the corresponding image will disappear from the list.</p>

<h3 id="possible-improvements-1">Possible Improvements</h3>

<p>As usual, there are improvements that could be made to this if you’re so inclined and your application is able to reuse this component elsewhere if it is more generic or customizable.</p>

<p>First of all, you could manage the styles better. I know that I didn’t post the styles here, but if you copied them from GitHub and you’re a person that cares a lot about which components control which styles, then you may be thinking that it’d be wiser to have some specific files moved out of this component. As with most of these possible improvements, this is mostly to do with making the component more useful in more situations. Some of the styles are very specific to how I wanted to display the previews for this one little app, but to make it more reusable, we either need to make styles customizable via props or pull them out and let an outer component define the styles.</p>

<p>Another possible change would be to add props that allow you to hide certain elements such as the button that fires the “remove” event. There are more elements coming later in the article that might be good to hide via props as well.</p>

<p>And finally, it might be wise to separate the <code>file</code> prop into multiple props such as <code>url</code>, <code>name</code>, and — as we’ll see later — <code>status</code>. This would allow this component to be used in situations where you just have an image URL and name rather than an <code>UploadableFile</code> instance, so it’s more useful in more situations.</p>

<h2 id="uploading-files">Uploading Files</h2>

<p>Alright, we have the drag and drop and a preview of the files selected, so now we need to upload those files and keep the user informed of the status of those uploads. We’ll start with creating a new file: <code>/compositions/file-uploader.js</code>. In this file, we’ll export some functions that allow our component to upload the files.</p>

<pre><code class="language-javascript">export async function uploadFile(file, url) {
    // set up the request data
    let formData = new FormData()
    formData.append('file', file.file)

    // track status and upload file
    file.status = 'loading'
    let response = await fetch(url, { method: 'POST', body: formData })

    // change status to indicate the success of the upload request
    file.status = response.ok

    return response
}

export function uploadFiles(files, url) {
    return Promise.all(files.map((file) =&gt; uploadFile(file, url)))
}

export default function createUploader(url) {
    return {
        uploadFile: function (file) {
            return uploadFile(file, url)
        },
        uploadFiles: function (files) {
            return uploadFiles(files, url)
        },
    }
}</code></pre>

<p>Before looking into specific functions, note that every function in this file is exported separately so it can be used on its own, but you’ll see that we’ll only be using one of them in our application. This gives some flexibility in how this module is used without actually making the code any more complicated since all we do is add an <code>export</code> statement to enable it.</p>

<p>Now, starting at the top, we have an asynchronous function for uploading a single file. This is constructed in a very similar manner to how it was done in the previous article, but we are using an <code>async</code> function instead (for that wonderful <code>await</code> keyword) and we’re updating the <code>status</code> property on the provided <code>file</code> to keep track of the upload’s progress. This <code>status</code> can have 4 possible values:</p>

<ul>
<li><code>null</code>: initial value; indicates that it has not started uploading;</li>
<li><code>&quot;loading&quot;</code>: indicates that the upload is in progress;</li>
<li><code>true</code>: indicates the upload was successful;</li>
<li><code>false</code>: indicates the upload failed.</li>
</ul>

<p>So, when we start the upload, we mark the status as <code>&quot;loading&quot;</code>. Once it’s finished, we mark it as <code>true</code> or <code>false</code> depending on the result’s <code>ok</code> property. Soon we’ll be using these values to show different messages in the <code>FilePreview</code> component. Finally, we return the response in case the caller can use that information.</p>

<p><strong>Note</strong>: <em>Depending on which service you upload your files to, you may need some additional headers for authorization or something, but you can get those from the documentation for those services since I can’t write an example for every service out there.</em></p>

<p>The next function, <code>uploadFiles</code>, is there to allow you to easily upload an array of files. The final function, <code>createUploader</code>, is a function that grants you the ability to use the other functions without having to specify the URL that you’re uploading to every time you call it. It “caches” the URL via a closure and returns versions of each of the two previous functions that don’t require the URL parameter to be passed in.</p>

<h3 id="using-the-uploader">Using the Uploader</h3>

<p>Now that we have these functions defined, we need to use them, so go back to our main app component. Somewhere in the <code>script</code> section, we’ll need to add the following two lines:</p>

<pre><code class="language-javascript">import  createUploader  from  './compositions/file-uploader'
const { uploadFiles } = createUploader('YOUR URL HERE')</code></pre>

<p>Of course, you’ll need to change the URL to match whatever your upload server uses. Now we just need to call <code>uploadFiles</code> from somewhere, so let’s add a button that calls it in its click handler. Add the following at the end of the template:</p>

<pre><code class="language-javascript">&lt;button @click.prevent="uploadFiles(files)"  class="upload-button"&gt;Upload&lt;/button&gt;</code></pre>

<p>There you go. Now if you run the app, add some images, and smash that button, they should be headed for the server. But… we can’t tell if it worked or not — at least not without checking the server or the network panel in the dev tools. Let’s fix that.</p>

<h3 id="showing-the-status">Showing The Status</h3>

<p>Open up <code>FilePreview.vue</code>. In the template after the <code>img</code> tag but still within <code>component</code>, let’s add the following:</p>

<pre><code class="language-javascript">&lt;span class="status-indicator loading-indicator" v-show="file.status == 'loading'"&gt;In Progress&lt;/span&gt;
&lt;span class="status-indicator success-indicator" v-show="file.status == true"&gt;Uploaded&lt;/span&gt;
&lt;span class="status-indicator failure-indicator" v-show="file.status == false"&gt;Error&lt;/span&gt;</code></pre>

<p>All the styles are already included to control how these look if you copied the styles from GitHub earlier. These all sit in the bottom right corner of the images displaying the current status. Only one of them is shown at a time based on <code>file.status</code>.</p>

<p>I used <code>v-show</code> here, but it also makes a lot of sense to use <code>v-if</code>, so you can use either one. By using <code>v-show</code>, it always has the elements in the DOM but hides them. This means we can inspect the elements and cause them to show up even if they aren’t in the correct state, so we can test if they look right without trying to do it by putting the app into a certain state. Alternatively, you could go into the Vue DevTools, make sure you’re in the “Inspector” screen, click the three dots menu button in the top right and toggle “Editable props” to true, then edit the props or state in the component(s) to bring about the states needed to test each indicator.</p>

<p><strong>Note</strong>: <em>Just be aware that once you edit the <code>file</code> state/prop, it is no longer the same object as the one that was passed in, so clicking the button to remove the image will not work (can’t remove a file that isn’t in the array) and clicking “Upload” won’t show any state changes for that image (because the one in the array that is being uploaded isn’t the same file object as the one being displayed by the preview).</em></p>

<h3 id="possible-improvements-2">Possible Improvements</h3>

<p>As with other parts of this app, there are a few things we could do to make this better, but that we won’t actually be changing. First of all, the status values are pretty ambiguous. It would be a good idea to implement the values as constants or an enum (TypeScript supports enums). This would ensure that you don’t misspell a value such as “loading” or try to set the status to “error” instead of false and run into a bug. The status could also be implemented as a state machine since there is a very defined set of rules for how the state changes.</p>

<p>In addition to better statuses, there should be better error handling. We inform the users that there was an issue with the upload, but they have no idea what the error is. Is it a problem with their internet? Was the file too big? Is the server down? Who knows? Users need to know what the problem is so they know what they can do about it — if anything.</p>

<p>We could also keep the users better apprised of the upload. By using XHR instead of <code>fetch</code> (which I discussed in the previous drag-and-drop uploader article), we can track “progress” events to know the percentage of the upload that was completed, which is very useful for large files and slow internet connections because it can prove to the user that progress is actually being made and that it didn’t get stuck.</p>

<p>The one change that can increase the reusability of the code is opening up the file uploader to additional options (such as request headers) to be able to be passed in. In addition, we could check the status of a file to prevent us from uploading a file that’s already in progress or is already uploaded. To further help with this, we could disable the “Upload” button during the upload, and it should probably also be disabled when there are no files selected.</p>

<p>And last, but most certainly not least, we should add some accessibility improvements. In particular, when adding files, removing them, and uploading them (with all those status changes), we should audibly inform screen reader users that things have changed using <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions">Live Regions</a>. I’m no expert on this, and they fall a bit outside the scope of this article, so I will not be going into any kind of detail, but it’s definitely something everyone should look into.</p>

<h2 id="job-s-done">Job’s Done</h2>

<p>Well, that’s it. The Vue Drag-and-Drop Image Uploader is done! As mentioned at the beginning, you can see <a href="https://vue-dd-uploader.pages.dev/">the finished product here</a> and look at the final code in the <a href="https://github.com/joezimjs/vue-dd-uploader">GitHub Repository</a>.</p>

<p>I hope you spend some time trying to implement the possible improvements that I’ve laid out in the previous sections to help you deepen your understanding of this app and keep sharpening your skills by thinking things through on your own. Do you have any other improvements that could be made to this uploader? Leave some suggestions in the comments and if you implemented any of the suggestions from above, you can share your work in the comments, too.</p>

<p>God bless and happy coding!</p>

<div class="partners__lead-place"></div>

<h3 id="further-reading">Further Reading</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2015/10/increase-workflow-reduce-stress-with-nature-sounds/">How To Increase Workflow And Reduce Stress With Nature Sounds</a></li>
<li><a href="https://www.smashingmagazine.com/2014/06/take-a-digital-health-check/">Take A Digital Health Check</a></li>
<li><a href="https://www.smashingmagazine.com/2015/08/designing-custom-images-online-publishing-faster/">Designing Custom Images For Your Online Content, Faster!</a></li>
<li><a href="https://www.smashingmagazine.com/2015/09/stop-being-an-afterthought/">Hey Designers: Stop Being An Afterthought</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(vf, yk, il, mrn)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Joel Olawanle</author><title>An Introduction To Quasar Framework: Building Cross-Platform Applications</title><link>https://www.smashingmagazine.com/2021/10/introduction-quasar-framework-cross-platform-applications/</link><pubDate>Sat, 23 Oct 2021 11:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2021/10/introduction-quasar-framework-cross-platform-applications/</guid><description>Quasar is an open-source Vue.js-based cross-platform framework that allows you, as a developer, to easily build apps for both desktop and mobile using technologies such as Cordova and Electron and writing your code once. The app we’ll build will store and get its data from Firebase, meaning that we will also be seeing how to use Firebase in Quasar.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2021/10/introduction-quasar-framework-cross-platform-applications/" />
              <title>An Introduction To Quasar Framework: Building Cross-Platform Applications</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>An Introduction To Quasar Framework: Building Cross-Platform Applications</h1>
                  
                    
                    <address>Joel Olawanle</address>
                  
                  <time datetime="2021-10-23T11:00:00&#43;00:00" class="op-published">2021-10-23T11:00:00+00:00</time>
                  <time datetime="2021-10-23T11:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>In this article, I will explain how to use Quasar Framework and its state-of-the-art UI (which follows Material guidelines) to build a notes app. The app will get its data from and store its data to Firebase. This tutorial is meant for anyone interested in building cross-platform applications that work well across all devices simultaneously. At the end of the article, you should have a proper understanding of what Quasar is, how to get started creating apps with Quasar, and also how to use Firebase.</p>

<p>To follow along in this article, you should have:</p>

<ul>
<li>an understanding of HTML, CSS, and JavaScript;</li>
<li>at least a little experience with <a href="https://vuejs.org/">Vue.js</a>;</li>
<li>Node.js version 10 or above and npm version 5 or above installed on your machine.</li>
<li>knowledge of how the command-line interface (CLI) works.</li>
</ul>

<p>The deployed app is <a href="https://quasar-notess-app.netlify.app/">available for viewing</a>, and the final code is <a href="https://github.com/olawanlejoel/Quasar-notes-app">on Github</a>.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="what-is-quasar-framework">What Is Quasar Framework?</h2>

<p>Quasar Framework is an open-source Vue.js-based cross-platform framework whose <a href="https://quasar.dev/introduction-to-quasar">motto is</a>: “write code once and simultaneously deploy it as a website, a mobile app and/or an Electron app”. It has many features that enable you, as a web developer, to build apps on desktop and mobile and to create progressive web apps (PWAs) using technologies such as Cordova, Electron, and the web (Vue.js).</p>

<h2 id="why-quasar-framework">Why Quasar Framework?</h2>

<p>Quasar is an easy-to-use but powerful UI kit comprising a lot of UI components, layout elements, and helpers. Together, these elements provide a full-featured toolset for building responsive front-end apps, without your having to make use of many different UI libraries. It does the heavy lifting for you, allowing you to focus on features and not boilerplate.</p>

<p>In summary, Quasar offers support for many build modes, including:</p>

<ul>
<li>single-page applications;</li>
<li>progressive web applications;</li>
<li>server-side rendering;</li>
<li>mobile apps (iOS and Android), using Cordova or Сapacitor;</li>
<li>multi-platform desktop apps, using Electron;</li>
<li>browser extensions.</li>
</ul>

<h2 id="getting-started">Getting Started</h2>

<p>To get started, let’s look at how to install Quasar on your local machine and set up a project.</p>

<h2 id="installation">Installation</h2>

<p>There are three ways to start using Quasar:</p>

<ul>
<li>embedding to an existing project via a content delivery network (CDN);</li>
<li>installing using the Vue.js CLI Quasar plugin;</li>
<li>installing using the Quasar CLI.</li>
</ul>

<p>For this tutorial, we will be using the third method, which is the Quasar CLI. The first thing to do is install the Quasar CLI globally on your computer, or check whether it is installed by running the following commands in your CLI:</p>

<pre><code class="language-bash">quasar -v #check if quasar has been installed previously

yarn global add @quasar/cli
# or
npm install -g @quasar/cli</code></pre>

<p>Once this is done, you can now move on to setting up the project.</p>

<h2 id="project-set-up">Project Set-Up</h2>

<p>Run the following command in your CLI:</p>

<pre><code class="language-bash">quasar create &lt;folder_name&gt;</code></pre>

<p>Following this, you are going to be asked some questions. Here is my full configuration for the app we will be building.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3da4ae7a-6396-428b-bb01-b51d414a79f7/12-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="433"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3da4ae7a-6396-428b-bb01-b51d414a79f7/12-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3da4ae7a-6396-428b-bb01-b51d414a79f7/12-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3da4ae7a-6396-428b-bb01-b51d414a79f7/12-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3da4ae7a-6396-428b-bb01-b51d414a79f7/12-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3da4ae7a-6396-428b-bb01-b51d414a79f7/12-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3da4ae7a-6396-428b-bb01-b51d414a79f7/12-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="Full configuration for our app"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3da4ae7a-6396-428b-bb01-b51d414a79f7/12-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Now we can move into the project folder and start up the application using the following commands:</p>

<pre><code class="language-bash">cd &lt;folder_name&gt;
quasar dev</code></pre>

<p>With the steps above complete, our app should be running on <a href="http://localhost:3000/">http://localhost:8080</a>. This is what we should see:</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/34125423-d35a-44df-9c31-a54f3d8d9a71/16-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="395"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/34125423-d35a-44df-9c31-a54f3d8d9a71/16-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/34125423-d35a-44df-9c31-a54f3d8d9a71/16-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/34125423-d35a-44df-9c31-a54f3d8d9a71/16-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/34125423-d35a-44df-9c31-a54f3d8d9a71/16-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/34125423-d35a-44df-9c31-a54f3d8d9a71/16-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/34125423-d35a-44df-9c31-a54f3d8d9a71/16-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="Quasar app"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/34125423-d35a-44df-9c31-a54f3d8d9a71/16-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="quasar-s-folder-structure">Quasar’s Folder Structure</h2>

<p>The default application structure for Quasar is intended to be a great starting point for developers to build any type of application. You can organize the application however you like and create directories whenever you need them.</p>

<pre><code class="language-bash">.
├── public/                  # pure static assets (directly copied)
├── src/
│   ├── assets/              # dynamic assets (processed by Webpack)
│   ├── components/          # .vue components used in pages and layouts
│   ├── css/                 # CSS/Stylus/Sass/… files for your app
│   ├── layouts/             # layout .vue files
│   ├── pages/               # page .vue files
│   ├── boot/                # boot files (app initialization code)
│   ├── router/              # Vue Router
│   ├── store/               # Vuex Store
│   ├── App.vue              # root Vue component of your app
│   └── index.template.html  # template for index.html
├── .editorconfig            # editor config
├── .gitignore               # GIT ignore paths
├── .postcssrc.js            # PostCSS config
├── babel.config.js          # Babel config
├── package.json             # npm scripts and dependencies
├── quasar.conf.js           # Quasar app config file
└── README.md                # readme for your app</code></pre>

<p>The source folder consists of about seven major directories that a beginner should care about:</p>

<ul>
<li><code>quasar.conf.js</code><br />
This is the brain behind any Quasar application, because most configurations are done in this file. Amazingly, Quasar handles most of the complex configurations needed by the various tools and packages that you might use in an application. Some of these configurations are for:

<ul>
<li>Quasar components, directives, and plugins that would be needed in your app;</li>
<li>icon packs;</li>
<li><a href="https://v0-17.quasar-framework.org/components/transition.html">CSS animation</a>;</li>
<li>PWA <a href="https://v0-17.quasar-framework.org/guide/pwa-configuring-pwa.html#Configuring-Manifest-File">manifest file</a> and <a href="https://v0-17.quasar-framework.org/guide/pwa-configuring-pwa.html#Quasar-conf-js">Workbox options</a>;</li>
<li><a href="https://v0-17.quasar-framework.org/guide/electron-configuring-electron.html">Electron packager</a> and/or <a href="https://quasar.dev/quasar-cli-vite/developing-electron-apps/build-commands/">Electron builder</a>;</li>
<li>and a <a href="https://v0-17.quasar-framework.org/guide/app-quasar.conf.js.html">lot more</a>.</li>
</ul></li>
<li><code>src/assets</code><br />
The <code>assets</code> directory contains your uncompiled assets, such as Stylus or Sass files, images, and fonts.</li>
<li><code>src/components</code><br />
This is where all of your reusable components will live. These components make up the different parts of the application and can be reused and imported into your pages, layouts, and even other components.</li>
<li><code>src/css</code><br />
You will not find this in Vue.js, but Quasar provides this so that we can have all of our global CSS in Sass form. It consists of two files: <code>app.sass</code> is where all of our styles will go, while <code>quasar.variables.sass</code> contains all of the reusable variables we would want to make use of when styling our app. You could ignore the CSS directory if you feel it’s of no use to you.</li>
<li><code>src/layouts</code><br />
This helps us create defined layouts for an app without repeating code. This is useful when you want to include sidebars or fixed bottom bars or have distinct layouts for mobile and desktop.</li>
<li><code>src/pages</code><br />
The <code>pages</code> directory contains our application’s views and routes. Our pages are injected into the app and managed through Vue Router in <code>/src/router/routes.js</code>. This means that each page needs to be referenced there.</li>
<li><code>src/router</code><br />
This holds the routing configuration of our app. It consists of two folders:

<ul>
<li><code>/src/router/index.js</code> holds the Vue Router initialization code.</li>
<li><code>/src/router/routes.js</code> holds the routes of the app, loading our layouts alongside the routes or pages in the app.<br />
You might not need to do anything to the <code>index.js</code> file in a small project, but if your project will have routes, you will need to add them to the <code>routes.js</code> file.</li>
</ul></li>
</ul>

<h2 id="building-a-notes-app">Building A Notes App</h2>

<p>When building an application with Quasar, the first thing we will want to do is create a layout. Quasar has made this process a lot easier than any other framework by making use of a layout builder. For our notes app, we will want something like the following, which is quite similar to the default layout but with a few modifications:</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b39fdbbf-1ba7-4e07-ac7d-9e220e75b540/9-introduction-quasar-framework-cross-platform-applications.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="547"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b39fdbbf-1ba7-4e07-ac7d-9e220e75b540/9-introduction-quasar-framework-cross-platform-applications.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b39fdbbf-1ba7-4e07-ac7d-9e220e75b540/9-introduction-quasar-framework-cross-platform-applications.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b39fdbbf-1ba7-4e07-ac7d-9e220e75b540/9-introduction-quasar-framework-cross-platform-applications.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b39fdbbf-1ba7-4e07-ac7d-9e220e75b540/9-introduction-quasar-framework-cross-platform-applications.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b39fdbbf-1ba7-4e07-ac7d-9e220e75b540/9-introduction-quasar-framework-cross-platform-applications.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b39fdbbf-1ba7-4e07-ac7d-9e220e75b540/9-introduction-quasar-framework-cross-platform-applications.png"
			
			sizes="100vw"
			alt="Building a notes app"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b39fdbbf-1ba7-4e07-ac7d-9e220e75b540/9-introduction-quasar-framework-cross-platform-applications.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="app-layout">App Layout</h2>

<p>In the sidebar of <a href="https://quasar.dev/">Quasar’s documentation</a>, you will see the “Layout and Grid” option. When you click it, a dropdown will appear with more options, one of which is “<a href="https://quasar.dev/layout-builder">Layout Builder</a>”. Click on “Layout Builder”, which will bring you here:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5615634b-74ac-4097-a3a8-6da9bd184824/15-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="394"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5615634b-74ac-4097-a3a8-6da9bd184824/15-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5615634b-74ac-4097-a3a8-6da9bd184824/15-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5615634b-74ac-4097-a3a8-6da9bd184824/15-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5615634b-74ac-4097-a3a8-6da9bd184824/15-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5615634b-74ac-4097-a3a8-6da9bd184824/15-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5615634b-74ac-4097-a3a8-6da9bd184824/15-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="App layout"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5615634b-74ac-4097-a3a8-6da9bd184824/15-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<p>This allows us to choose the options we want and remove the ones we don’t. Then, we would generate the code to paste in the layout file.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2a089ac2-1d0e-4fd3-a3c8-120a5c51da67/1-introduction-quasar-framework-cross-platform-applications.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="451"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2a089ac2-1d0e-4fd3-a3c8-120a5c51da67/1-introduction-quasar-framework-cross-platform-applications.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2a089ac2-1d0e-4fd3-a3c8-120a5c51da67/1-introduction-quasar-framework-cross-platform-applications.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2a089ac2-1d0e-4fd3-a3c8-120a5c51da67/1-introduction-quasar-framework-cross-platform-applications.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2a089ac2-1d0e-4fd3-a3c8-120a5c51da67/1-introduction-quasar-framework-cross-platform-applications.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2a089ac2-1d0e-4fd3-a3c8-120a5c51da67/1-introduction-quasar-framework-cross-platform-applications.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2a089ac2-1d0e-4fd3-a3c8-120a5c51da67/1-introduction-quasar-framework-cross-platform-applications.png"
			
			sizes="100vw"
			alt="App layout"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2a089ac2-1d0e-4fd3-a3c8-120a5c51da67/1-introduction-quasar-framework-cross-platform-applications.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The first option helps us to pick the layout parts, while the second allows us to configure the layout parts. Finally, we export the generated layout.</p>

<p>If you want the exact same layout as mine, use the code below:</p>

<pre><code class="language-javascript">&lt;template&gt;
  &lt;q-layout view="lHh lpR fFf"&gt;
    &lt;q-header elevated class="bg-primary text-white"&gt;
      &lt;q-toolbar&gt;
        &lt;q-btn dense flat round icon="menu" @click="left = !left" /&gt;
        &lt;q-toolbar-title&gt;
          &lt;q-avatar&gt;
            &lt;img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg" /&gt;
          &lt;/q-avatar&gt;
          Title
        &lt;/q-toolbar-title&gt;
      &lt;/q-toolbar&gt;
    &lt;/q-header&gt;
    &lt;q-drawer show-if-above v-model="left" side="left" bordered&gt;
      &lt;!-- drawer content --&gt;
    &lt;/q-drawer&gt;
    &lt;q-page-container&gt;
      &lt;router-view /&gt;
    &lt;/q-page-container&gt;
  &lt;/q-layout&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      left: false
    };
  }
};
&lt;/script&gt;
</code></pre>

<p>Remove the default layout and paste the code above or the code that you have generated into <code>src/layouts/MainLayout.vue</code>.</p>

<p>The code above is divided into three sections: the header (navbar), the drawer (sidebar), and the page container (which contains the router view).</p>

<p>We’ve made use of the state-of-the-art UI to style the whole page. As I said earlier, when using Quasar, you won’t need additional heavy libraries such as Hammer.js, Moment.js, or Bootstrap.</p>

<p>We will be adding data to the sidebar and editing the navbar. Once you’ve saved, you will notice our app now looks like this:</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27d57fb2-80cb-4c46-8850-7edf5e23e0e9/2-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="395"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27d57fb2-80cb-4c46-8850-7edf5e23e0e9/2-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27d57fb2-80cb-4c46-8850-7edf5e23e0e9/2-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27d57fb2-80cb-4c46-8850-7edf5e23e0e9/2-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27d57fb2-80cb-4c46-8850-7edf5e23e0e9/2-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27d57fb2-80cb-4c46-8850-7edf5e23e0e9/2-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27d57fb2-80cb-4c46-8850-7edf5e23e0e9/2-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="Quasar app"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27d57fb2-80cb-4c46-8850-7edf5e23e0e9/2-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Let’s work on the layout, adding some items to the sidebar and changing the title of the app, If you scan the layout code that we added, you will see where we are supposed to edit and add these various items.</p>

<p>Here is what my layout looks like after I’ve added items to the sidebar and changed the title:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7eda12f3-df15-4b82-ba3d-29158b409341/3-introduction-quasar-framework-cross-platform-applications.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="451"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7eda12f3-df15-4b82-ba3d-29158b409341/3-introduction-quasar-framework-cross-platform-applications.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7eda12f3-df15-4b82-ba3d-29158b409341/3-introduction-quasar-framework-cross-platform-applications.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7eda12f3-df15-4b82-ba3d-29158b409341/3-introduction-quasar-framework-cross-platform-applications.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7eda12f3-df15-4b82-ba3d-29158b409341/3-introduction-quasar-framework-cross-platform-applications.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7eda12f3-df15-4b82-ba3d-29158b409341/3-introduction-quasar-framework-cross-platform-applications.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7eda12f3-df15-4b82-ba3d-29158b409341/3-introduction-quasar-framework-cross-platform-applications.png"
			
			sizes="100vw"
			alt="Quasar layout"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/7eda12f3-df15-4b82-ba3d-29158b409341/3-introduction-quasar-framework-cross-platform-applications.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>And here is the code:</p>

<pre><code class="language-javascript">&lt;template&gt;
  &lt;q-layout view="lHh lpR fFf"&gt;
    &lt;q-header elevated class="bg-primary text-white"&gt;
      &lt;q-toolbar&gt;
        &lt;q-btn dense flat round icon="menu" @click="left = !left" /&gt;
        &lt;q-toolbar-title class="text-h6"&gt;
          My Notes
        &lt;/q-toolbar-title&gt;
      &lt;/q-toolbar&gt;
    &lt;/q-header&gt;
    &lt;q-drawer show-if-above v-model="left" side="left" bordered&gt;
      &lt;q-list class="q-pt-xl"&gt;
        &lt;q-item clickable v-ripple to="/"&gt;
          &lt;q-item-section avatar&gt;
            &lt;q-icon name="home" size="md" /&gt;
          &lt;/q-item-section&gt;
          &lt;q-item-section class="text-weight-bold"&gt;Home&lt;/q-item-section&gt;
        &lt;/q-item&gt;
        &lt;q-item clickable v-ripple to="/about"&gt;
          &lt;q-item-section avatar&gt;
            &lt;q-icon name="info" size="md" /&gt;
          &lt;/q-item-section&gt;
          &lt;q-item-section class="text-weight-bold"&gt;About&lt;/q-item-section&gt;
        &lt;/q-item&gt;
      &lt;/q-list&gt;
    &lt;/q-drawer&gt;
    &lt;q-page-container&gt;
      &lt;router-view /&gt;
    &lt;/q-page-container&gt;
    &lt;q-footer class="bg-grey-2 text-black "&gt;
      &lt;q-toolbar&gt;
        &lt;q-toolbar-title class="text-subtitle2"&gt;
          Designed and Built For this article.
        &lt;/q-toolbar-title&gt;
      &lt;/q-toolbar&gt;
    &lt;/q-footer&gt;
  &lt;/q-layout&gt;
&lt;/template&gt;
&lt;script&gt;
export default {
  data() {
    return {
      left: false
    };
  }
};
&lt;/script&gt;</code></pre>

<p>We removed the logo in the navbar and edited the text, then added a list to the sidebar, making use of Quasar’s state-of-the-art UI. Check out the list items, and copy the code of any you wish to use.</p>

<h2 id="app-design">App Design</h2>

<p>Earlier on, I said I was going to use Quasar’s state-of-the-art UI (which follows Material guidelines) to build a notes app, and that’s what we will be doing now. Explaining the whole process in an article like this is difficult, but the “Style &amp; Identity” section of Quasar’s documentation covers it well.</p>

<p>This will be a one-page app (<code>index.vue</code>), and here is the code, without any styling applied:</p>

<pre><code class="language-javascript">&lt;template&gt;
  &lt;q-page class=""&gt;
    &lt;div class="q-pa-md"&gt;
      &lt;q-input
        bottom-slots
        v-model="newNoteContent"
        placeholder="Write your note here..."
        counter
        autogrow
        maxlength="300"
      &gt;
        &lt;template v-slot:after&gt;
          &lt;q-btn round dense flat icon="send" /&gt;
        &lt;/template&gt;
      &lt;/q-input&gt;
    &lt;/div&gt;
    &lt;q-separator size="10px" /&gt;
    &lt;q-list bordered class="rounded-borders" style="max-width: 600px"&gt;
      &lt;q-item-label header&gt;You have 3 Note(s)&lt;/q-item-label&gt;
      &lt;div&gt;
        &lt;q-item&gt;
          &lt;q-item-section top&gt;
            &lt;q-item-label caption class="text-grey-9"&gt;
              He who has imagination without learning has wings but no feet.
            &lt;/q-item-label&gt;
          &lt;/q-item-section&gt;
          &lt;q-item-section top side&gt;
            &lt;div class="text-grey-9 q-gutter-xs"&gt;
              &lt;q-btn size="13px" flat dense round icon="delete" /&gt;
            &lt;/div&gt;
          &lt;/q-item-section&gt;
        &lt;/q-item&gt;
        &lt;q-separator size="1px" /&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;q-item&gt;
          &lt;q-item-section top&gt;
            &lt;q-item-label caption class="text-grey-9"&gt;
              He who has imagination without learning has wings but no feet.
            &lt;/q-item-label&gt;
          &lt;/q-item-section&gt;
          &lt;q-item-section top side&gt;
            &lt;div class="text-grey-9 q-gutter-xs"&gt;
              &lt;q-btn size="13px" flat dense round icon="delete" /&gt;
            &lt;/div&gt;
          &lt;/q-item-section&gt;
        &lt;/q-item&gt;
        &lt;q-separator size="1px" /&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;q-item&gt;
          &lt;q-item-section top&gt;
            &lt;q-item-label caption class="text-grey-9"&gt;
              He who has imagination without learning has wings but no feet.
            &lt;/q-item-label&gt;
          &lt;/q-item-section&gt;
          &lt;q-item-section top side&gt;
            &lt;div class="text-grey-9 q-gutter-xs"&gt;
              &lt;q-btn size="13px" flat dense round icon="delete" /&gt;
            &lt;/div&gt;
          &lt;/q-item-section&gt;
        &lt;/q-item&gt;
        &lt;q-separator size="1px" /&gt;
      &lt;/div&gt;
    &lt;/q-list&gt;
  &lt;/q-page&gt;
&lt;/template&gt;
&lt;script&gt;
import db from "src/boot/firebase";
export default {
  name: "PageIndex",
  data() {
    return {
      basic: false,
      fixed: false,
      newNoteContent: ""
    };
  }
};
&lt;/script&gt;
</code></pre>

<p>In the code above, we have an <a href="https://quasar.dev/vue-components/input">input field</a> from Quasar. We’ve attached a <code>v-model</code> to get the data from the input field once the “Submit” button is clicked. We also have a list of items that will be used to display each note, and each list item has an icon used to delete that particular item when clicked.</p>

<h2 id="setting-up-local-data">Setting Up Local Data</h2>

<p>At this point, the design of our app is in place. The next thing we will do is create an array that would contain all of our notes. We will ensure that we can add to and delete from this array before setting up Firebase.</p>

<p>Here is the array that we will be making use of in our app for now. Later, we will remove this array or comment out the code.</p>

<pre><code class="language-javascript">notes: [
  {
    id: 1,
    noteContent: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Ea vereprehenderit aspernatur mollitia saepe cupiditate pariatur natus accusantium esse repudiandae nisi velit provident corporis commodi eius fugiat reiciendis non aliquam."
  },
  {
    id: 2,
    noteContent: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Ea vereprehenderit aspernatur mollitia saepe cupiditate pariatur natus accusantium esse repudiandae nisi velit provident corporis commodi eius fugiat reiciendis non aliquam."
  },
  {
    id: 3,
    noteContent: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Ea vereprehenderit aspernatur mollitia saepe cupiditate pariatur natus accusantium esse repudiandae nisi velit provident corporis commodi eius fugiat reiciendis non aliquam."
  }
]</code></pre>

<div class="partners__lead-place"></div>

<h3 id="fetching-data">Fetching Data</h3>

<p>We now have our array. Let’s add these data to our app. Because we understand Vue.js, all we will do is loop through this array using the <code>v-for</code> directive, use the data gotten from the array, and then put the content wherever we want it to appear.</p>

<pre><code class="language-javascript">&lt;div v-for="noteContent in notes" :key="noteContent.id"&gt;
  &lt;q-item&gt;
    &lt;q-item-section top&gt;
      &lt;q-item-label caption class="text-grey-9"&gt;
        {{ noteContent.note }}
      &lt;/q-item-label&gt;
    &lt;/q-item-section&gt;
    &lt;q-item-section top side&gt;
      &lt;div class="text-grey-9 q-gutter-xs"&gt;
        &lt;q-btn
          size="13px"
          flat
          dense
          round
          icon="delete"
          @click="deleteNote(noteContent)"
        /&gt;
      &lt;/div&gt;
    &lt;/q-item-section&gt;
  &lt;/q-item&gt;
  &lt;q-separator size="1px" /&gt;
&lt;/div&gt;</code></pre>

<p>We also added a click event handler to the delete button, so that it loads this function whenever it’s created.</p>

<h3 id="adding-notes">Adding Notes</h3>

<p>Let’s see how to add notes to our app by using the input field. We will use JavaScript’s <code>unShift()</code> methods, which adds one or more elements to the beginning of an array and returns the new length of the array.</p>

<p>The first thing to do is to add a click event handler to the button.</p>

<pre><code class="language-javascript">&lt;q-btn round dense flat icon="send" @click="addNote" /&gt;</code></pre>

<p>Then, proceed to create this method in the script area.</p>

<pre><code class="language-javascript">methods: {
  addNote() {
    let newNote = {
      id: this.notes.length + 1,
     note: this.newNoteContent
    };
    this.notes.unshift(newNote);
    this.newNoteContent = "";
  }
}</code></pre>

<p>In the code above, we created an object for the new note, which comprises the ID and the note itself, and then we added this <code>newNote</code> to the array of <code>notes</code> via the <code>unShift()</code> method.</p>

<h3 id="deleting-notes">Deleting Notes</h3>

<p>Finally, before proceeding to use Firebase in our app, let’s see how to delete a note. The first thing would be to add an event listener to the delete icon:</p>

<pre><code class="language-javascript">&lt;q-btn
  size="13px"
  flat
  dense
  round
  icon="delete"
  @click="deleteNote(noteContent)"
/&gt;</code></pre>

<p>And then we would create a method:</p>

<pre><code class="language-javascript">deleteNote(noteContent) {
  let noteId = noteContent.id;

  //doing this to get the real id of the notes
  let index = this.notes.findIndex(noteContent => noteContent.id === noteId);
  this.notes.splice(index, 1);
}</code></pre>

<p>In this code, we got the <code>id</code> of the particular note that we want to delete through the parameter passed to the click event method that was created. Then, we made use of the <code>splice</code> method to remove only that item from the array.</p>

<h2 id="firebase">Firebase</h2>

<p>Now that these two pieces of functionality work, let’s now see how we can use Firebase in Quasar to add, fetch, and delete data. Firebase will also give us real-time data syncing across all devices. The data in our app won’t be very much, because it’s just for the purpose of learning. In case you are thinking of something big that would be used by millions of people, check out the <a href="https://firebase.google.com/pricing">pricing page</a>.</p>

<p>Firebase is application development software from Google that enables us to develop iOS, Android, and web apps.</p>

<h3 id="setting-up-cloud-firestore">Setting Up Cloud Firestore</h3>

<p>To get started, visit <a href="https://firebase.google.com">firebase.google.com</a> and click on either the “Go to console” link in the top-right corner of your screen or the “Get started” button (ensure that you sign in with your Google account).</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fbcf729a-7644-4a40-83d8-4728e65e4d22/11-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="398"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fbcf729a-7644-4a40-83d8-4728e65e4d22/11-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fbcf729a-7644-4a40-83d8-4728e65e4d22/11-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fbcf729a-7644-4a40-83d8-4728e65e4d22/11-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fbcf729a-7644-4a40-83d8-4728e65e4d22/11-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fbcf729a-7644-4a40-83d8-4728e65e4d22/11-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fbcf729a-7644-4a40-83d8-4728e65e4d22/11-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="Firebase website"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fbcf729a-7644-4a40-83d8-4728e65e4d22/11-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<p>This will bring us to the <a href="https://console.firebase.google.com/u/0/">console area</a>, where we can create a project. Click on the “Add a project” button, and a form to create your project will appear. The first question will request the project’s name, which could be anything; for this article, we will call it “notesApp”. Let’s also disable Google Analytics because ours is a mini-app.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a9813cb8-19a2-414c-a16e-5d9ad518fe81/8-introduction-quasar-framework-cross-platform-applications.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="480"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a9813cb8-19a2-414c-a16e-5d9ad518fe81/8-introduction-quasar-framework-cross-platform-applications.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a9813cb8-19a2-414c-a16e-5d9ad518fe81/8-introduction-quasar-framework-cross-platform-applications.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a9813cb8-19a2-414c-a16e-5d9ad518fe81/8-introduction-quasar-framework-cross-platform-applications.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a9813cb8-19a2-414c-a16e-5d9ad518fe81/8-introduction-quasar-framework-cross-platform-applications.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a9813cb8-19a2-414c-a16e-5d9ad518fe81/8-introduction-quasar-framework-cross-platform-applications.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a9813cb8-19a2-414c-a16e-5d9ad518fe81/8-introduction-quasar-framework-cross-platform-applications.png"
			
			sizes="100vw"
			alt="Google Analytics for your Firebase project"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a9813cb8-19a2-414c-a16e-5d9ad518fe81/8-introduction-quasar-framework-cross-platform-applications.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Click on the “Create project” button (this might take few seconds, so be patient). Then, click on “Continue”, so that we can create our cloud Firestore.</p>

<p>In the sidebar, click on “Firestore”, and then “Create database”.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f0525960-b2d9-43dc-bddf-9c0c9dbd11ba/5-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="396"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f0525960-b2d9-43dc-bddf-9c0c9dbd11ba/5-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f0525960-b2d9-43dc-bddf-9c0c9dbd11ba/5-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f0525960-b2d9-43dc-bddf-9c0c9dbd11ba/5-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f0525960-b2d9-43dc-bddf-9c0c9dbd11ba/5-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f0525960-b2d9-43dc-bddf-9c0c9dbd11ba/5-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f0525960-b2d9-43dc-bddf-9c0c9dbd11ba/5-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="Cloud Firestore"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f0525960-b2d9-43dc-bddf-9c0c9dbd11ba/5-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<p>This will bring up a modal. Click on “Start in test mode”, which will make it easy for us to start working with our database. Bear in mind that, “The default security rules for test mode allow anyone with your database reference to view, edit and delete all data in your database for the next 30 days”.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/079915ec-ba53-4fbf-9683-8b028729a008/18-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="544"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/079915ec-ba53-4fbf-9683-8b028729a008/18-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/079915ec-ba53-4fbf-9683-8b028729a008/18-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/079915ec-ba53-4fbf-9683-8b028729a008/18-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/079915ec-ba53-4fbf-9683-8b028729a008/18-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/079915ec-ba53-4fbf-9683-8b028729a008/18-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/079915ec-ba53-4fbf-9683-8b028729a008/18-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="Create database"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/079915ec-ba53-4fbf-9683-8b028729a008/18-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Click on “Next”, leave the default Cloud Firestore location, and then click on the “Enable” button. Once it loads, our database will be fully ready for us to use.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e3552ecd-0870-4634-9f8a-c4d505569ab0/10-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="395"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e3552ecd-0870-4634-9f8a-c4d505569ab0/10-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e3552ecd-0870-4634-9f8a-c4d505569ab0/10-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e3552ecd-0870-4634-9f8a-c4d505569ab0/10-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e3552ecd-0870-4634-9f8a-c4d505569ab0/10-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e3552ecd-0870-4634-9f8a-c4d505569ab0/10-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e3552ecd-0870-4634-9f8a-c4d505569ab0/10-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="Cloud Firestore"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e3552ecd-0870-4634-9f8a-c4d505569ab0/10-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<p><strong>Note:</strong> The Firebase database is made up of collections, and these collections contain documents, and each document is a JavaScript object that has fields in it.</p>

<p>Let’s get started by creating a new collection for our notes.</p>

<p>To create a collection, click on “Start collection”. A modal will pop up for you to enter the collection ID — meaning, a name. Then, click on the “Next” button.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b31be315-7b72-4a89-b4d0-3032cc61ef5e/7-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="439"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b31be315-7b72-4a89-b4d0-3032cc61ef5e/7-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b31be315-7b72-4a89-b4d0-3032cc61ef5e/7-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b31be315-7b72-4a89-b4d0-3032cc61ef5e/7-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b31be315-7b72-4a89-b4d0-3032cc61ef5e/7-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b31be315-7b72-4a89-b4d0-3032cc61ef5e/7-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b31be315-7b72-4a89-b4d0-3032cc61ef5e/7-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="Click on “Start collection”"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b31be315-7b72-4a89-b4d0-3032cc61ef5e/7-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<p>You can now start creating the documents and fields for each note. Auto-generate the ID of the document to ensure that it is unique by clicking “Auto-ID” beside the document field.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/aafd5d2f-27df-4204-a711-4383d7bd6d5e/13-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="722"
			height="452"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/aafd5d2f-27df-4204-a711-4383d7bd6d5e/13-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/aafd5d2f-27df-4204-a711-4383d7bd6d5e/13-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/aafd5d2f-27df-4204-a711-4383d7bd6d5e/13-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/aafd5d2f-27df-4204-a711-4383d7bd6d5e/13-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/aafd5d2f-27df-4204-a711-4383d7bd6d5e/13-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/aafd5d2f-27df-4204-a711-4383d7bd6d5e/13-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="Add a document"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/aafd5d2f-27df-4204-a711-4383d7bd6d5e/13-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Click “Save”, and continue to create more documents. In the end, this is what my database looks like:</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5b71b559-d0d6-4b4e-a9f3-4a003ad79edc/6-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="427"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5b71b559-d0d6-4b4e-a9f3-4a003ad79edc/6-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5b71b559-d0d6-4b4e-a9f3-4a003ad79edc/6-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5b71b559-d0d6-4b4e-a9f3-4a003ad79edc/6-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5b71b559-d0d6-4b4e-a9f3-4a003ad79edc/6-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5b71b559-d0d6-4b4e-a9f3-4a003ad79edc/6-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5b71b559-d0d6-4b4e-a9f3-4a003ad79edc/6-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="Database"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/5b71b559-d0d6-4b4e-a9f3-4a003ad79edc/6-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Now that we are done, let’s see how to connect Firebase to our app. Go to “Project overview” in the sidebar, and let’s add this to a web app by clicking the “Web” button.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f7e52e47-5852-47ab-9be6-9bd524f6ee44/14-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="396"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f7e52e47-5852-47ab-9be6-9bd524f6ee44/14-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f7e52e47-5852-47ab-9be6-9bd524f6ee44/14-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f7e52e47-5852-47ab-9be6-9bd524f6ee44/14-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f7e52e47-5852-47ab-9be6-9bd524f6ee44/14-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f7e52e47-5852-47ab-9be6-9bd524f6ee44/14-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f7e52e47-5852-47ab-9be6-9bd524f6ee44/14-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="Go to “Project overview” in the sidebar, and add this to a web app by clicking the “Web” button"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f7e52e47-5852-47ab-9be6-9bd524f6ee44/14-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<p>A form will appear for us to “Add Firebase” to our web app. We will give it the same name, “notesApp”, and register the app (don’t check the “Firebase hosting” field).</p>

<p>Once it has loaded, it will bring up an SDK to help us initialize our database in the app. We won’t do it this way, although we will need some information from the generated SDK. The right way to do this in Quasar is to import the modules that we need and use a boot file.</p>

<p>So, leave that tab open, and let’s see how to add the Firebase SDK and initialize Firebase in our Quasar app.</p>

<p>The first thing to do would be to install Firebase in our project with npm.</p>

<pre><code class="language-bash">npm install --save firebase</code></pre>

<p>Once installation is complete, we are going to initialize our app’s connection to Firebase by creating a boot file, so that we have immediate access to the database when our app is ready.</p>

<p>A boot file helps us to run code before the app’s Vue.js root component is instantiated. Quasar’s documentation has more information about <a href="https://quasar.dev/quasar-cli/boot-files#introduction">boot files</a> and <a href="https://quasar.dev/quasar-cli/boot-files#when-to-use-boot-files">when to use boot files</a>.</p>

<p>To generate a boot file, we will run this command in our CLI:</p>

<pre><code class="language-bash">quasar new boot firebase</code></pre>

<p><strong>Note:</strong> You don’t need to use Firebase as the name of the boot file.</p>

<p>Once this is done, you will notice that the file is now created in the <code>boot</code> folder. To make use of this newly created boot file, we’ll need to add it to the <code>quasar.config.js</code> file’s boot array.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/55ee3862-7f27-46f1-b600-435b05b87ea7/4-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="393"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/55ee3862-7f27-46f1-b600-435b05b87ea7/4-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/55ee3862-7f27-46f1-b600-435b05b87ea7/4-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/55ee3862-7f27-46f1-b600-435b05b87ea7/4-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/55ee3862-7f27-46f1-b600-435b05b87ea7/4-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/55ee3862-7f27-46f1-b600-435b05b87ea7/4-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/55ee3862-7f27-46f1-b600-435b05b87ea7/4-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="quasar.config.js file’s boot array"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/55ee3862-7f27-46f1-b600-435b05b87ea7/4-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Let’s go back to the newly created boot file. Delete all of the code there because we don’t need it. We’ll import the modules that we need and configure our database. Paste in the following code:</p>

<pre><code class="language-javascript">import firebase from "firebase/app";
import "firebase/firestore";

const firebaseConfig = {
  // ...
};

// Initialize Firebase
firebase.initializeApp(firebaseConfig);</code></pre>

<p>Here, we’ve imported Firebase itself and Firestore, and we’ve initialized Firebase, making use of the config, which we will be adding now.</p>

<p>At this point, we are almost done configuring our app. We need to add our unique configuration, which was provided in the SDK that was generated when we added Firebase to our web app. Copy only the configuration, and paste it into our array.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/68ec212d-348a-4601-8393-0348c67cd0aa/17-introduction-quasar-framework-cross-platform-applications.JPG">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="531"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/68ec212d-348a-4601-8393-0348c67cd0aa/17-introduction-quasar-framework-cross-platform-applications.JPG 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/68ec212d-348a-4601-8393-0348c67cd0aa/17-introduction-quasar-framework-cross-platform-applications.JPG 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/68ec212d-348a-4601-8393-0348c67cd0aa/17-introduction-quasar-framework-cross-platform-applications.JPG 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/68ec212d-348a-4601-8393-0348c67cd0aa/17-introduction-quasar-framework-cross-platform-applications.JPG 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/68ec212d-348a-4601-8393-0348c67cd0aa/17-introduction-quasar-framework-cross-platform-applications.JPG 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/68ec212d-348a-4601-8393-0348c67cd0aa/17-introduction-quasar-framework-cross-platform-applications.JPG"
			
			sizes="100vw"
			alt="Add Firebase SDK"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/68ec212d-348a-4601-8393-0348c67cd0aa/17-introduction-quasar-framework-cross-platform-applications.JPG'>Large preview</a>)
    </figcaption>
  
</figure>

<p>We should now have something like this:</p>

<pre><code class="language-javascript">import firebase from "firebase/app";
import "firebase/firestore";
const firebaseConfig = {
  apiKey: "***",
  authDomain: "notesapp-ffd7c.firebaseapp.com",
  projectId: "notesapp-ffd7c",
  storageBucket: "notesapp-ffd7c.appspot.com",
  messagingSenderId: "18944010047",
  appId: "1:18944010047:web:ddfb46fc6bc8bba375158a"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);</code></pre>

<p>One last thing, since we are making use of Firestore, is that we’ll need to initialize the cloud Firestore by adding this code to our boot file (the last line):</p>

<pre><code class="language-javascript">let db = firebase.firestore();
export default db;</code></pre>

<p>This <code>db</code> will give us access to our Firestore database. We also exported it so that we can use it anywhere in our app.</p>

<p>At this point, you might still be a little confused, but if you have followed this guide, then you will have properly initialized Firebase for your application. You can read more about adding Firebase to your JavaScript project <a href="https://firebase.google.com/docs/web/setup?authuser=0">in the documentation</a>.</p>

<h3 id="fetching-data-from-firebase">Fetching Data From Firebase</h3>

<p>If you have followed the guide so far, everything should work fine once you launch your app. Now let’s grab the notes created in the database and display them in our app.</p>

<p>For this, we will be making use of the <code>.onSnapshot</code> hook, which will be fired any time the data in our collection changes. This will tell us whether a document has been added, removed, or updated. For this guide, we will only deal with the addition and removal of documents. Using hooks like this makes real-time syncing across devices possible. Let’s get started.</p>

<p>The first thing to do would be for us to get access to the database by importing it into the index page of our app.</p>

<pre><code class="language-javascript">import db from 'src/boot/firebase';</code></pre>

<p>Then, create a mounted hook, because we want to fetch our data immediately after the app has loaded.</p>

<pre><code class="language-javascript">mounted() {
  db.collection("notes").onSnapshot(snapshot => {
    snapshot.docChanges().forEach(change => {

      let noteChange = change.doc.data();

      if (change.type === "added") {
        console.log("New note: ", noteChange);
        this.notes.unshift(noteChange);
      }
      if (change.type === "modified") {
        console.log("Modified note: ", noteChange);
      }
      if (change.type === "removed") {
        console.log("Removed note: ", noteChange);
      }
    });
  });
}</code></pre>

<p>In the code above, we are simply grabbing our <code>notes</code> collection, and every time there is a change in the collection, the <code>onSnapShot</code> method will be fired, which will return a <code>snapShot</code> of all our data. All of these data will be objects with <code>type</code> properties. These <code>type</code> properties will tell us the type of change that has happened and give us access to the data that was either added, modified, or removed.</p>

<p>This might sound confusing, but you will understand what we are doing as you read on.</p>

<p>If you save your code and check the console environment, you will notice that each note has been logged out. We can now push these objects to the <code>notes</code> array that we created earlier, so that we can display real-time data in our application.</p>

<p>The first thing to do is delete or comment out the objects in the <code>notes</code> array, so that we have something like this:</p>

<pre><code class="language-javascript">notes: []</code></pre>

<p>Then, pass the objects to this array:</p>

<pre><code class="language-javascript">this.notes.unshift(noteChange);</code></pre>

<p>Your code should now look like this:</p>

<pre><code class="language-javascript">if (change.type === "added") {
  this.notes.unshift(noteChange);
}</code></pre>

<p>At this point, if you load the app, you will notice that you have successfully fetched your data from Firebase.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ed1be4bb-89ca-4e2d-a3ab-74a24f3b5469/19-introduction-quasar-framework-cross-platform-applications.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="552"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ed1be4bb-89ca-4e2d-a3ab-74a24f3b5469/19-introduction-quasar-framework-cross-platform-applications.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ed1be4bb-89ca-4e2d-a3ab-74a24f3b5469/19-introduction-quasar-framework-cross-platform-applications.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ed1be4bb-89ca-4e2d-a3ab-74a24f3b5469/19-introduction-quasar-framework-cross-platform-applications.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ed1be4bb-89ca-4e2d-a3ab-74a24f3b5469/19-introduction-quasar-framework-cross-platform-applications.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ed1be4bb-89ca-4e2d-a3ab-74a24f3b5469/19-introduction-quasar-framework-cross-platform-applications.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ed1be4bb-89ca-4e2d-a3ab-74a24f3b5469/19-introduction-quasar-framework-cross-platform-applications.png"
			
			sizes="100vw"
			alt="App with fetched data from Firebase"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ed1be4bb-89ca-4e2d-a3ab-74a24f3b5469/19-introduction-quasar-framework-cross-platform-applications.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="adding-data-to-firebase">Adding Data To Firebase</h3>

<p>Let’s see how to add a note to our notes collection in this app. At this point, if you try to use the input field to add a note, it will work but the note will disappear once you refresh the page because it’s not stored in any database.</p>

<p>To do this with Firebase, all that is needed is to update the <code>addNote()</code> method that we created earlier.</p>

<pre><code class="language-javascript">addNote() {
  let newNote = {
    // id: this.notes.length + 1,
    note: this.newNoteContent
  };
  // this.notes.unshift(newNote);

  db.collection("notes")
    .add(newNote)
    .then(docRef =&gt; {
      console.log("Document written with ID: ", docRef.id);
    })
    .catch(error => {
      console.error("Error adding document: ", error);
    });

  this.newNoteContent = "";
},</code></pre>

<p>The first thing we did here was remove the ID that is used when we made use of the previous array, because we are now going to auto-generate the ID from Firebase. We also removed the <code>unshift()</code> method; it’s no longer useful because data is being fetched for our app once there is an update using the <code>snapShot</code> hook.</p>

<p>If we look at the code responsible for updating the Firestore <code>db</code>, all we are passing to the collection (<code>notes</code>) is the new object (<code>newNote</code>), and this function will automatically generate an ID for each of our documents. The documentation has more information on <a href="https://firebase.google.com/docs/firestore/manage-data/add-data#add_a_document">adding data to Firebase</a>.</p>

<h3 id="deleting-data-from-firebase">Deleting Data From Firebase</h3>

<p>We are almost done with our app, but we need to be able to delete data in our app from Firebase. Currently, the <code>delete</code> function works, but if you reload the app, the deleted data will reappear.</p>

<p>As we did before, we are going to delete these data (or documents) from Firebase using the unique ID generated by Firebase.</p>

<p>Currently, we don’t have access to the ID. To access it, we will add it to the <code>noteChange</code> object:</p>

<pre><code class="language-javascript">noteChange.id = change.doc.id;</code></pre>

<p>Once that is set, deleting data will be as easy as adding it. All we have to do is go to the <code>deleteNote(noteContent)</code> method that we created previously, delete the previous code, and make use of this:</p>

<pre><code class="language-javascript">deleteNote(noteContent) {
  let noteId = noteContent.id;
  db.collection("notes")
    .doc(noteId)
    .delete()
    .then(() =&gt; {
      console.log("Document successfully deleted!");
    })
    .catch(error =&gt; {
      console.error("Error removing document: ", error);
    });
}</code></pre>

<p>This checks the notes collection for a document with the specified ID and then deletes it. But if we save our code now and click the delete icon, the data will delete but won’t leave the app’s interface unless we refresh our code, meaning that the <code>snapshot</code> hook needs to be updated. Go to the <code>snapshot</code> hook for <code>removed</code>, and add this code:</p>

<pre><code class="language-javascript">if (change.type === "removed") {
  console.log("Removed note: ", noteChange);
  let index = this.notes.findIndex(
    noteContent =&gt; noteContent.id === noteChange.id
  );
  this.notes.splice(index, 1);
}</code></pre>

<p>This simply gets the ID of the post that we deleted and removes it from the interface.</p>

<p>With that done, we have built an app with Quasar that works with Firebase. One major advantage of Quasar is that it enables us to simultaneously deploy our project as a website, mobile app, or Electron app.</p>

<p>To deploy for iOS, <a href="https://cordova.apache.org/">Cordova</a> needs to be installed on our local machine. A MacBook is highly preferable. Navigate to your CLI, and install Cordova globally:</p>

<pre><code class="language-bash">$ npm install - g cordova</code></pre>

<p>To install on Windows, you would make use of Electron. The documentation <a href="https://quasar.dev/quasar-cli/developing-cordova-apps/preparation#android-setup">properly explains how to do this</a>.</p>

<h2 id="conclusion">Conclusion</h2>

<p>In this guide, we have built a notes application using Quasar and Firebase. By following this guide, you are now in a position to improve on and add your own features and functionality. Here are a few ideas to get you started:</p>

<ul>
<li>Implement functionality to modify notes.</li>
<li>Add dates, so that you can order the data by date.</li>
<li>Style the app, and make it more creative.</li>
<li>Add images.</li>
<li>A lot more.</li>
</ul>

<h3 id="useful-resources">Useful Resources</h3>

<ul>
<li><a href="https://quasar.dev/">Quasar documentation</a></li>
<li><a href="https://firebase.google.com/docs/firestore">Firebase documentation</a></li>
</ul>

<div class="partners__lead-place"></div>

<h3 id="further-reading">Further Reading</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2023/05/boost-design-workflow-setapp/">How To Boost Your Design Workflow With Setapp</a></li>
<li><a href="https://www.smashingmagazine.com/2021/10/composable-css-animation-vue-animxyz/">Composable CSS Animation In Vue With AnimXYZ</a></li>
<li><a href="https://www.smashingmagazine.com/2024/05/netlify-platform-primitives/">The Era Of Platform Primitives Is Finally Here</a></li>
<li><a href="https://www.smashingmagazine.com/2024/12/creating-effective-multistep-form-better-user-experience/">Creating An Effective Multistep Form For Better User Experience</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(ks, vf, yk, il, al, mrn)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Ejiro Asiuwhu</author><title>Composable CSS Animation In Vue With AnimXYZ</title><link>https://www.smashingmagazine.com/2021/10/composable-css-animation-vue-animxyz/</link><pubDate>Sat, 09 Oct 2021 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2021/10/composable-css-animation-vue-animxyz/</guid><description>Most animation libraries like GSAP and Framer Motion are built purely with JavaScript or TypeScript, unlike AnimXYZ, which is labelled “the first composable CSS animation toolkit”, built mainly with SCSS. While a simple library, it can be used to achieve a lot of awesome animation on the web in a short amount of time and little code. In this article, Ejiro Asiuwhu will show you how to use the AnimXYZ toolkit to create unique, interactive, and visually engaging animations in Vue.js and plain HTML.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2021/10/composable-css-animation-vue-animxyz/" />
              <title>Composable CSS Animation In Vue With AnimXYZ</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Composable CSS Animation In Vue With AnimXYZ</h1>
                  
                    
                    <address>Ejiro Asiuwhu</address>
                  
                  <time datetime="2021-10-09T10:00:00&#43;00:00" class="op-published">2021-10-09T10:00:00+00:00</time>
                  <time datetime="2021-10-09T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>In this article, you will learn how to use the AnimXYZ toolkit to create unique, interactive, and visually engaging animations in Vue.js and plain HTML. By the end of this article, you will have learned how adding a few CSS classes to elements in Vue.js components can give you a lot of control over how those elements move in the DOM.</p>

<p>This tutorial will be beneficial to readers who are interested in creating interactive animations with few lines of code.</p>

<p><strong>Note</strong>: <em>This article requires a basic understanding of Vue.js and CSS.</em></p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h3 id="what-is-animxyz">What Is AnimXYZ?</h3>

<p><a href="https://animxyz.com/">AnimXYZ</a> is a composable, performant, and customizable CSS animation toolkit powered by CSS variables. It is designed to enable you to create awesome and unique animations without writing a line of CSS keyframes. Under the hood, it uses <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties">CSS variables</a> to create custom CSS properties. The nice thing about AnymXYZ is its declarative approach. An element can be animated in one of two ways: when entering or leaving the page. If you want to animate an HTML element with this toolkit, adding a class of <code>xyz-out</code> will animate the item out of the page, while <code>xyz-in</code> will animate the component into the page.</p>

<p>This awesome toolkit can be used in a regular HTML project, as well as in a Vue.js or React app. However, as of the time of writing, <strong>support for React is still under development</strong>.</p>

<h3 id="why-use-animxyz">Why Use AnimXYZ?</h3>

<h4 id="composable">Composable</h4>

<p>Animation with AnimXYZ is possible by adding descriptive class names to your markup. This makes it easy to <strong>write complex CSS animation without writing complex CSS keyframes</strong>. Animating an element into the page is as easy as adding a class of <code>xyz-in</code> in the component and declaring a descriptive attribute.</p>

<div class="break-out">

<pre><code class="language-html">&lt;p class="xyz-in" xyz="fade"&gt;Composable CSS animation with AnimXYZ&lt;/p&gt;</code></pre>

</div>

<p>The code above will make the paragraph element fade into the page, while the code below will make the element fade out of the page. Just a single class with a lot of power.</p>

<pre><code class="language-html">&lt;p class="intro xyz-out" xyz="fade"&gt;Composable CSS animation with AnimXYZ&lt;/p&gt;</code></pre>

<h4 id="customizable">Customizable</h4>

<p>For simple animations, you can use the out-of-the-box <a href="https://animxyz.com/docs#utilities">utilities</a>, but AnimXYZ can do so much more. You can customize and control AnimXYZ to create exactly the animations you want by setting the CSS variables that drive all AnimXYZ animations. We will create some custom animations later on in this tutorial.</p>

<h4 id="performant">Performant</h4>

<p>With AnimXYZ, you can create powerful and smooth animations out of the box, and its size is only 2.68 KB for the base functionality and 11.4 KB if you include the convenient utilities.</p>

<h4 id="easy-to-learn-and-use">Easy to Learn and Use</h4>

<p>AnimXYZ works perfectly with regular HTML and CSS, and it can be integrated in a project using the content delivery network (CDN) link. It can also be used in Vue.js and React, although support for React is still under development. Also, the learning curve with this toolkit is not steep when compared to animation libraries such as <a href="https://greensock.com/gsap/">GSAP</a> and <a href="https://www.framer.com/motion/">Framer Motion</a>, and the official documentation makes it easy to get started because it explains how the package works in simple terms.</p>

<h3 id="key-concepts-in-animxyz">Key Concepts in AnimXYZ</h3>

<h4 id="contexts">Contexts</h4>

<p>When you want a particular flow of animation to be applied to related groups of element, the <code>xyz</code> attribute provides the context. Let’s say you want three <code>div</code>s to be animated in the same way when they enter the page. All you have to do is add the <code>xyz</code> attribute to the parent element, with the composable utilities and variable that you want to apply.</p>

<div class="break-out">

<pre><code class="language-html">&lt;div class="shape-wrapper xyz-in" xyz="fade flip-up flip-left"&gt;
  &lt;div class="shape"&gt;&lt;/div&gt;
  &lt;div class="shape"&gt;&lt;/div&gt;
  &lt;div class="shape"&gt;&lt;/div&gt;
&lt;/div&gt;</code></pre>

</div>

<p>The code above will apply the same animation to all <code>div</code>s with a class of <code>shape</code>. All child elements will fade into the page and flip to the upper left, because the attribute <code>xyz=&quot;fade flip-up flip-left&quot;</code> has been applied to the parent element.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="abyoqdY"
	data-user="smashingmag"
	data-default-tab="html,result"
	class="codepen">See the Pen [Contexts in AnimXYZ](https://codepen.io/smashingmag/pen/abyoqdY) by <a href="https://codepen.io/ejiro-asiuwhuw">Ejiro Asiuwhu</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/abyoqdY">Contexts in AnimXYZ</a> by <a href="https://codepen.io/ejiro-asiuwhuw">Ejiro Asiuwhu</a>.</figcaption>
</figure>

<p>AnimXYZ makes it easy to animate a child element differently from its parent. To achieve this, add the <code>xyz</code> attribute with a different animation variable and different utilities to the child element, which will reset all animation properties that it has inherited from its parent.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="porzayR"
	data-user="smashingmag"
	data-default-tab="html,resul"
	class="codepen">See the Pen [Override Parent contexts in AnimXYZ](https://codepen.io/smashingmag/pen/porzayR) by <a href="https://codepen.io/ejiro-asiuwhu">Ejiro Asiuwhu</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/porzayR">Override Parent contexts in AnimXYZ</a> by <a href="https://codepen.io/ejiro-asiuwhu">Ejiro Asiuwhu</a>.</figcaption>
</figure>

<h4 id="utilities">Utilities</h4>

<p>AnimXYZ comes with a lot of utilities that will enable you to create engaging and powerful CSS animations without writing any custom CSS.</p>

<div class="break-out">

<pre><code class="language-css">xyz="fade up in-left in-rotate-left out-right out-rotate-right"</code></pre>

</div>

<p>For example, the code above has a <code>fade up</code> utility, which will make the element fade from top to bottom when coming into the page. It will come in and rotate from the left. When the element leaves the page, it will go to the right and rotate out of the page.</p>

<p>With the out-of-the-box utilities, you can, say, flip a group of elements to the right and make them fade while leaving the page. The possibilities of what can be achieved with the utilities are endless.</p>

<h4 id="staggering">Staggering</h4>

<p>The <code>stagger</code> utility controls the <code>animation-delay</code> CSS property for each of the elements in a list, so that their animations are triggered one after another. It specifies the amount of time to wait between applying the animation to an element and beginning to perform the animation. Essentially, it is used to queue up animation so that elements can be animated in sequence.</p>

<div class="break-out">

<pre><code class="language-html">&lt;div class="shape-wrapper" xyz="fade up-100% origin-top flip-down flip-right-50% rotate-left-100% stagger"&gt;
  &lt;div class="shape xyz-in"&gt;&lt;/div&gt;
  &lt;div class="shape xyz-in"&gt;&lt;/div&gt;
  &lt;div class="shape xyz-in"&gt;&lt;/div&gt;
  &lt;div class="shape xyz-in"&gt;&lt;/div&gt;
&lt;/div&gt;</code></pre>

</div>

<p>By adding the <code>stagger</code> utility, each element in a parent <code>div</code> will animate one after another from left to right. The order can be revered by using <code>stagger-rev</code>.</p>

<p>With <code>stagger</code>:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="abyoqNG"
	data-user="smashingmag"
	data-default-tab="html,result"
	class="codepen">See the Pen [Staggering with AnimXYZ](https://codepen.io/smashingmag/pen/abyoqNG) by <a href="https://codepen.io/ejiro-asiuwhu">Ejiro Asiuwhu</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/abyoqNG">Staggering with AnimXYZ</a> by <a href="https://codepen.io/ejiro-asiuwhu">Ejiro Asiuwhu</a>.</figcaption>
</figure>

<p>Without <code>stagger</code>:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="BadBYzN"
	data-user="smashingmag"
	data-default-tab="html,result"
	class="codepen">See the Pen [!Staggering Animation - AnimXYZ](https://codepen.io/smashingmag/pen/BadBYzN) by <a href="https://codepen.io/ejiro-asiuwhu">Ejiro Asiuwhu</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/BadBYzN">!Staggering Animation - AnimXYZ</a> by <a href="https://codepen.io/ejiro-asiuwhu">Ejiro Asiuwhu</a>.</figcaption>
</figure>

<h3 id="using-animxyz-with-html-and-css">Using AnimXYZ With HTML and CSS</h3>

<p>Let’s build a card and add some cool animation with AnimeXYZ.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="jOLNZrV"
	data-user="smashingmag"
	data-default-tab="html,result"
	class="codepen">See the Pen [Animxyz Demo](https://codepen.io/smashingmag/pen/jOLNZrV) by <a href="https://codepen.io/ejiro-asiuwhu">Ejiro Asiuwhu</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/jOLNZrV">Animxyz Demo</a> by <a href="https://codepen.io/ejiro-asiuwhu">Ejiro Asiuwhu</a>.</figcaption>
</figure>

<p>First, we need to add the AnimXYZ toolkit to our project. The easiest way is via a CDN. <a href="https://animxyz.com/docs/#installation">Grab the CDN</a>, and add it to the <code>head</code> of your HTML document.</p>

<p>Add the following lines of code to your HTML.</p>

<div class="break-out">

<pre><code class="language-html">&lt;p class="intro xyz-in" xyz="fade"&gt;Composable CSS Animation with Animxyz&lt;/p&gt;
  &lt;div class="glass xyz-in" id="glass"
        xyz="fade flip-down flip-right-50%  duration-10"&gt;
        &lt;img src="https://cdn.dribbble.com/users/238864/screenshots/15043450/media/7160a9f4acc18f4ec2cbe38eb167de62.jpg"
            alt="" class="avatar xyz-in"&gt;
        &lt;p class="des xyz-in"&gt;Image by Jordon Cheung&lt;/p&gt;
  &lt;/div&gt;</code></pre>

</div>

<p>This is where the magic happens. At the top of the page, we have a paragraph tag with a class of <code>xyz-in</code> and an <code>xyz</code> attribute with a value of <code>fade</code>. This means that the <code>p</code> element will fade into the page.</p>

<p>Next, we have a card with an <code>id</code> of <code>glass</code>, with the following <code>xyz</code> attribute:</p>

<pre><code class="language-css">  xyz="fade flip-down flip-right-50%  duration-10"</code></pre>

<p>The composable utilities above will make the card fade into the page. The <code>flip-down</code> value will set the card to flip into the page from the bottom, and the <code>flip-right</code> value will flip the card by 50% when leaving the page. An animation duration of <code>10</code> (i.e. 1 second) sets the length of time that the animation will take to complete one cycle.</p>

<div class="partners__lead-place"></div>

<h3 id="integrating-animxyz-in-vue-js">Integrating AnimXYZ in Vue.js</h3>

<h4 id="scaffold-a-vue-js-project">Scaffold a Vue.js Project</h4>

<p>Using the Vue.js command-line interface (CLI), run the command below to generate the application:</p>

<pre><code class="language-bash">vue create animxyz-vue</code></pre>

<h4 id="install-vueanimxyz">Install VueAnimXYZ</h4>

<pre><code class="language-bash">npm install @animxyz/vue</code></pre>

<p>This will install both the core package and the Vue.js package. After installation, we will have to import the <code>VueAnimXYZ</code> package into our project and add the plugin globally to our Vue.js application. To do this, open your <code>main.js</code> file, and add the following block of code accordingly:</p>

<div class="break-out">

<pre><code class="language-javascript">import VueAnimXYZ from '@animxyz/vue'  // import AnimXZY vue package
import '@animxyz/core' // import AnimXZY core package

Vue.use(VueAnimXYZ)</code></pre>

</div>

<h4 id="the-xyztransition-component">The <code>XyzTransition</code> Component</h4>

<p>The <a href="https://animxyz.com/docs/#vue-xyz-transition"><code>XyzTransition</code> component</a> is built on top of Vue.js’ <a href="https://vuejs.org/v2/guide/transitions.html"><code>transition</code></a> component. It’s used to animate individual elements into and out of the page.</p>

<p><a href="https://codesandbox.io/embed/the-xyztransition-component-in-animxyz-o6wt1?fontsize=14&amp;hidenavigation=1&amp;theme=dark">Here is a demo</a> of how to use the <code>XyzTransition</code> component in Vue.js.</p>

<p>Notice that a lot of the complexity that comes with Vue.js’ <code>transition</code> component has been abstracted away in order to reduce complexity and increase efficiency. All we need to care about when using the <code>XyzTransition</code> component are the <code>appear</code>, <code>appear-visible</code>, <code>duration</code>, and <code>mode</code> props.</p>

<p>For a more detailed guide, check out the <a href="https://animxyz.com/docs/#vue-xyz-transition">official documentation</a>.</p>

<p>Let’s use the <code>XYZTransition</code> component to animate an element when a button is clicked.</p>

<div class="break-out">

<pre><code class="language-javascript">&lt;div id="app"&gt;
    &lt;button @click="isAnimate = !isAnimate"&gt;Animate&lt;/button&gt;
    &lt;XyzTransition
      appear
      xyz="fade up in-left in-rotate-left out-right out-rotate-right"
    &gt;
      &lt;div class="square" v-if="isAnimate"&gt;&lt;/div&gt;
    &lt;/XyzTransition&gt;
&lt;/div&gt;</code></pre>

</div>

<p>Notice how the element that we intend to transition is wrapped in the <code>XYZTransition</code> component. This is important because the child element <code>&lt;div class=&quot;square&quot; v-if=&quot;isAnimate&quot;&gt;&lt;/div&gt;</code> will inherit the utilities that are applied to the <code>XYZTransition</code> component. The child element is also conditionally rendered when <code>isAnimate</code> is set to <code>true</code>. When the button is clicked, the child element with a class of <code>square</code> is toggled into and out of the DOM.</p>

<h4 id="xyztransitiongroup"><code>XyzTransitionGroup</code></h4>

<p>The <code>XyzTransitionGroup</code> component is built on top of Vue.js’ <a href="https://vuejs.org/v2/api/#transition-group"><code>transition-group</code> component</a>. It is used to animate groups of elements into and out of the page.</p>

<p>Below is an illustration of how to use the <code>XyzTransitionGroup</code> component in Vue.js. Notice here again that a lot of the complexity that comes with Vue.js’ <code>transition-group</code> component has been abstracted away in order to reduce complexity and increase efficiency. All we need to care about when using the <code>XyzTransitionGroup</code> component are <code>appear</code>, <code>appear-visible</code>, <code>duration</code>, and <code>tag</code>. The following is taken <a href="https://animxyz.com/docs#vue-xyz-transition-group">from the documentation</a>:</p>

<div class="break-out">

<pre><code class="language-javascript">&lt;XyzTransitionGroup
  appear={ boolean }
  appear-visible={ boolean | IntersectionObserverOptions }
        duration={ number | 'auto' | { appear: number | 'auto', in: number | 'auto',
                   out: number | 'auto' } }
        tag={ string } &gt;
        &lt;child-component /&gt;
        &lt;child-component /&gt;
        &lt;child-component /&gt;
&lt;/XyzTransitionGroup&gt;</code></pre>

</div>

<div class="partners__lead-place"></div>

<h3 id="build-an-animated-modal-with-animxyz-and-vue-js">Build an Animated Modal With AnimXYZ and Vue.js</h3>

<p>Let’s build modal components that will animate as they enter and leave the DOM.</p>

<p>Here is a demo of what we are going to build:</p>

<figure><a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c1083eed-1672-479c-924f-3f37bb7e770a/animxyz-vue-demo.gif"><img src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c1083eed-1672-479c-924f-3f37bb7e770a/animxyz-vue-demo.gif" width="600" height="315" alt="Demo" /></a><figcaption>Demo</figcaption></figure>

<p>By adding the <code>xyz=&quot;fade out-delay-5&quot;</code> property to the <code>XyzTransition</code> component, the modal will fade.</p>

<p>Notice that we are adding <code>.xyz-nested</code> to almost all child elements of the modal component. This is because we want to trigger their animations when a modal component’s element is open.</p>

<p>The <code>ease-out-back</code> property that we added to the dialog container will add a slight overshoot when the dialog is opened and when it has been closed.</p>

<p>Adding <code>in-delay</code> to the child elements of the modal component will make the animation feel more natural, because the element will be delayed until the other contents of the modal have animated in:</p>

<div class="break-out">

<pre><code class="language-javascript">  &lt;section class="xyz-animate"&gt;
    &lt;div class="alerts__wrap copy-content"&gt;
      &lt;div class="alert reduced-motion-alert"&gt;
        &lt;p&gt;
          AnimXYZ animations are disabled if your browser or OS has
          reduced-motion setting turned on.
          &lt;a href="https://web.dev/prefers-reduced-motion/" target="_blank"&gt;
            Learn more here.
          &lt;/a&gt;
        &lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;h1&gt;Modal Animation With AnimXYZ and Vue.js&lt;/h1&gt;
    &lt;button
      class="modal-toggle modal-btn-main"
      data-modal-text="Open Modal"
      data-modal-title="Title"
      data-modal-close-text="Close"
      id="label_modal_kdf8e87cga"
      aria-haspopup="dialog"
      ref="openButton"
      @click="open"
      autofocus
    &gt;
      Open Modal
    &lt;/button&gt;
    &lt;span
      id="js-modal-overlay"
      class="simple-modal-overlay"
      data-background-click="enabled"
      title="Close this window"
      v-if="isModal"
      @click="close"
    &gt;
      &lt;span class="invisible"&gt;Close this window&lt;/span&gt;
    &lt;/span&gt;
    &lt;div
      role="dialog"
      class="simple-modal__wrapper"
      aria-labelledby="modal-title"
    &gt;
      &lt;XyzTransition duration="auto" xyz="fade out-delay-5"&gt;
        &lt;section
          id="modal1"
          aria-labelledby="modal1_label"
          aria-modal="true"
          class="modal xyz-nested"
          xyz="fade small stagger ease-out-back"
          v-if="isModal"
          tabindex="-1"
          ref="modal"
          @keydown.esc="close"
        &gt;
          &lt;div class="modal_top flex xyz-nested" xyz="up-100% in-delay-3"&gt;
            &lt;header
              id="modal1_label modal-title"
              class="modal_label xyz-nested"
              xyz="fade right in-delay-7"
            &gt;
              Join our community on Slack
            &lt;/header&gt;
            &lt;button
              type="button"
              aria-label="Close"
              xyz="fade small in-delay-7"
              class="xyz-nested"
              @click="close"
              title="Close"
            &gt;
              &lt;svg viewBox="0 0 24 24" focusable="false" aria-hidden="true"&gt;
                &lt;path
                  fill="currentColor"
                  d="M.439,21.44a1.5,1.5,0,0,0,2.122,2.121L11.823,14.3a.25.25,0,0,1,.354,0l9.262,9.263a1.5,1.5,0,1,0,2.122-2.121L14.3,12.177a.25.25,0,0,1,0-.354l9.263-9.262A1.5,1.5,0,0,0,21.439.44L12.177,9.7a.25.25,0,0,1-.354,0L2.561.44A1.5,1.5,0,0,0,.439,2.561L9.7,11.823a.25.25,0,0,1,0,.354Z"
                &gt;&lt;/path&gt;
              &lt;/svg&gt;
            &lt;/button&gt;
          &lt;/div&gt;
          &lt;div class="modal_body xyz-nested" xyz="up-100% in-delay-3"&gt;
            &lt;div class="modal_body--top flex justify_center align_center"&gt;
              &lt;img
                src="../assets/slack.png"
                alt="slack logo"
                class="slack_logo"
              /&gt;
              &lt;img src="../assets/plus.png" alt="plus" class="plus" /&gt;
              &lt;img
                src="../assets/discord.png"
                alt="discord logo"
                class="discord_logo"
              /&gt;
            &lt;/div&gt;
            &lt;p&gt;&lt;span class="bold"&gt;929&lt;/span&gt; users are registered so far.&lt;/p&gt;
          &lt;/div&gt;
          &lt;form class="modal_form" autocomplete&gt;
            &lt;label for="email"
              &gt;&lt;span class="sr-only"&gt;Enter your email&lt;/span&gt;&lt;/label
            &gt;
            &lt;input
              id="email"
              type="email"
              placeholder="johndoe@email.com"
              autocomplete="email"
              aria-describedby="email"
              class="modal_input"
              required
            /&gt;
            &lt;button type="submit" class="modal_invite_btn"&gt;
              Get my invite
            &lt;/button&gt;
            &lt;p&gt;Already joined?&lt;/p&gt;
            &lt;button
              type="button"
              aria-describedby="open_slack"
              class="
                modal_slack_btn
                flex
                align_center
                justify_center
                xyz-nested
              "
              xyz="fade in-right in-delay-7"
              id="open_slack"
            &gt;
              &lt;span
                &gt;&lt;img src="../assets/slack.png" alt="slack logo" role="icon"
              /&gt;&lt;/span&gt;
              Open Slack
            &lt;/button&gt;
          &lt;/form&gt;
        &lt;/section&gt;
      &lt;/XyzTransition&gt;
    &lt;/div&gt;
  &lt;/section&gt;</code></pre>

</div>

<p>Then, in our modal, we would use the <code>v-if=&quot;isModal&quot;</code> directive to specify that we want the modal to be hidden from the page by default. Then, when the button is clicked, we open the modal by calling the <code>open()</code> method, which sets the <code>isModal</code> property to <code>true</code>. This will reveal the modal on the page and also apply the animation properties that we specified using AnimXYZ’s built-in utilities.</p>

<pre><code class="language-javascript">&lt;script&gt;
export default {
  data() {
    return {
      isModal: false,
    };
  },
  methods: {
    open() {
      if (!this.isModal) {
        this.isModal = true;
        this.$nextTick(() => {
          const modalRef = this.$refs.modal;
          console.log(modalRef);
          modalRef.focus();
        });
      }
    },
    close() {
      if (this.isModal) {
        this.isModal = false;
        this.$nextTick(() => {
          const openButtonRef = this.$refs.openButton;
          openButtonRef.focus();
        });
      }
    },
  },
};
&lt;/script&gt;</code></pre>

<p>AnimXYZ’s animations are disabled when the reduced-motion setting in the browser or operating system is turned on. Let’s display a helper message to users who have opted in to reduced motion.</p>

<p>Using the <code>@media screen and (prefers-reduced-motion)</code> media query, we’ll display a message notifying those users that they’ve turned off the animation feature in our modal component. To do this, add the following block of code to your styles:</p>

<pre><code class="language-css">&lt;style&gt;
@media (prefers-reduced-motion: reduce) {
  .alerts__wrap {
    display: block;
  }
}
&lt;/style&gt;
</code></pre>

<figure><a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/1bc8f0b1-92b8-42e2-ac1c-f150d255b1b8/animxyz-vue-demo-reduced-motion.gif"><img src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/1bc8f0b1-92b8-42e2-ac1c-f150d255b1b8/animxyz-vue-demo-reduced-motion.gif" width="600" height="311" alt="AnimXYZ animations when reduced-motion setting is turned on" /></a><figcaption>AnimXYZ animations when reduced-motion setting is turned on.</figcaption></figure>

<iframe src="https://codesandbox.io/embed/animated-modal-with-animxyz-lm7vm?fontsize=14&hidenavigation=1&theme=dark"
     style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
     title="Animated Modal With AnimXYZ"
     allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
     sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
   ></iframe>

<h3 id="conclusion">Conclusion</h3>

<p>We’ve gone through the basics of AnimXYZ and how to use it with plain HTML and Vue.js. We’ve also implemented some demo projects that give us a glimpse of the range of CSS animations that we can create simply by adding the composable utility classes provided by this toolkit, and all without writing a single line of a CSS keyframe. Hopefully, this tutorial has given you a solid foundation to add some sleek CSS animations to your own projects and to build on them over time for any of your needs.</p>

<p>The final demo <a href="https://github.com/ejirocodes/AnimXYZ-Vue">is on GitHub</a>. Feel free to clone it and try out the toolkit for yourself.</p>

<p>That’s all for now! Let me know in the comments section below what you think of this article. I am active on <a href="https://twitter.com/ejirocodes">Twitter</a> and <a href="https://github.com/ejirocodes">GitHub</a>. Thank you for reading, and stay tuned.</p>

<h4 id="resources">Resources</h4>

<ul>
<li><a href="https://animxyz.com/docs/">Documentation</a>, AnimXYZ</li>
<li><a href="https://css-tricks.com/animxyz/">AnimXYZ</a>, Chris Coyier, CSS Tricks</li>
</ul>

<div class="partners__lead-place"></div>

<h3 id="further-reading">Further Reading</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2023/05/big-tooling-upgrades-large-organizations/">How To Deal With Big Tooling Upgrades In Large Organizations</a></li>
<li><a href="https://www.smashingmagazine.com/2024/09/sticky-headers-full-height-elements-tricky-combination/">Sticky Headers And Full-Height Elements: A Tricky Combination</a></li>
<li><a href="https://www.smashingmagazine.com/2024/12/introduction-css-scroll-driven-animations/">An Introduction To CSS Scroll-Driven Animations: Scroll And View Progress Timelines</a></li>
<li><a href="https://www.smashingmagazine.com/2024/12/the-design-leader-dilemma/">The Design Leader Dilemma</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(ks, vf, yk, il, al, mrn)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Miracle Onyenma</author><title>How To Implement Search Functionality In Your Nuxt App Using Algolia InstantSearch</title><link>https://www.smashingmagazine.com/2021/10/search-functionality-nuxt-app-algolia-instantsearch/</link><pubDate>Fri, 01 Oct 2021 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2021/10/search-functionality-nuxt-app-algolia-instantsearch/</guid><description>Many websites have some sort of search feature because it helps users navigate through their content easily. Implementing it the right way can be tricky and might not give a good user experience. In this tutorial, Miracle Onyenma will be integrating Algolia, a popular and powerful search service for the best experience on our Nuxt site.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2021/10/search-functionality-nuxt-app-algolia-instantsearch/" />
              <title>How To Implement Search Functionality In Your Nuxt App Using Algolia InstantSearch</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>How To Implement Search Functionality In Your Nuxt App Using Algolia InstantSearch</h1>
                  
                    
                    <address>Miracle Onyenma</address>
                  
                  <time datetime="2021-10-01T10:00:00&#43;00:00" class="op-published">2021-10-01T10:00:00+00:00</time>
                  <time datetime="2021-10-01T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>Giving users the ability to quickly search through and navigate our content easily comes with great benefits. This not only improves the user experience, but also increases user retention and boosts conversion as users can now explore beyond what brought them to our site in the first place.</p>

<p>In this tutorial, we’ll be looking at how to integrate this search functionality into our Nuxt app using Algolia. Algolia is a third-party service that we can integrate into our app and provides us with a set of tools that allow us to create a full search experience in our sites and applications.</p>

<p>We’ll be using Nuxt Content, &ldquo;Git Based Headless CMS&rdquo; which allows us to create and manage content using Markdown, XML, JSON files, and so on. We’ll build a Nuxt site with Nuxt Content with a search feature using Algolia InstantSearch, for styling, we’ll use TailwindCSS. This tutorial is aimed at Vue.js devs that are familiar with Nuxt.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="prerequisites">Prerequisites</h2>

<p>To follow along with this tutorial, you’ll need to have the following installed:</p>

<ul>
<li><a href="https://nodejs.org/">Node</a>,</li>
<li>A text editor, I recommend <a href="https://code.visualstudio.com/">VS Code</a> with the <a href="https://marketplace.visualstudio.com/items?itemName=octref.vetur">Vetur</a> extension (for Vue.js syntax features in VS Code),</li>
<li>A terminal, you can use VS Code’s integrated terminal or any other of your choice.</li>
</ul>

<p>You’ll also require a basic understanding of the following in order to follow along smoothly:</p>

<ul>
<li>HTML, CSS &amp; JavaScript,</li>
<li><a href="https://vuejs.org/">Vue.js</a>,</li>
<li><a href="https://nuxtjs.org/">Nuxt.js</a>,</li>
<li><a href="https://tailwindcss.com/">TailwindCSS</a>.</li>
</ul>

<h2 id="setting-up-our-nuxt-app">Setting Up Our Nuxt App</h2>

<p>Nuxt.js is a framework built on Vue, it has many capabilities and features including Server-Side Rendering (SSR).</p>

<p>To install it, open  our terminal and run:</p>

<pre><code class="language-bash">npx create-nuxt-app &lt;project-name&gt;</code></pre>

<blockquote>
<p>&ldquo;Where <code>&lt;project-name&gt;</code> is the name of our project folder, I’ll be using <code>algolia-nuxt</code> for this project.&rdquo;</p>
</blockquote>

<p>Running the command will ask you some questions (name, Nuxt options, UI framework, TypeScript, etc. ). To find out more about all the options, see the <a href="https://github.com/nuxt/create-nuxt-app/blob/master/README.md">Create Nuxt app</a>.</p>

<p>When asked for Nuxt.js modules, make sure to select <code>Content - Git-based headless CMS</code> to install the <code>nuxt/content</code> module along with our Nuxt app.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/665121ea-a8ff-4099-a2fb-1725b1a2f943/nuxt-installation-in-terminal.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="309"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/665121ea-a8ff-4099-a2fb-1725b1a2f943/nuxt-installation-in-terminal.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/665121ea-a8ff-4099-a2fb-1725b1a2f943/nuxt-installation-in-terminal.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/665121ea-a8ff-4099-a2fb-1725b1a2f943/nuxt-installation-in-terminal.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/665121ea-a8ff-4099-a2fb-1725b1a2f943/nuxt-installation-in-terminal.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/665121ea-a8ff-4099-a2fb-1725b1a2f943/nuxt-installation-in-terminal.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/665121ea-a8ff-4099-a2fb-1725b1a2f943/nuxt-installation-in-terminal.png"
			
			sizes="100vw"
			alt="Nuxt installation in terminal."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Nuxt installation. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/665121ea-a8ff-4099-a2fb-1725b1a2f943/nuxt-installation-in-terminal.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>After selecting all of your options, installation can begin. My selected options look like this:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/73cc1fb3-0424-4760-a512-9f9fbb0a3329/nuxt-installation-options-in-terminal.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="364"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/73cc1fb3-0424-4760-a512-9f9fbb0a3329/nuxt-installation-options-in-terminal.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/73cc1fb3-0424-4760-a512-9f9fbb0a3329/nuxt-installation-options-in-terminal.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/73cc1fb3-0424-4760-a512-9f9fbb0a3329/nuxt-installation-options-in-terminal.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/73cc1fb3-0424-4760-a512-9f9fbb0a3329/nuxt-installation-options-in-terminal.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/73cc1fb3-0424-4760-a512-9f9fbb0a3329/nuxt-installation-options-in-terminal.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/73cc1fb3-0424-4760-a512-9f9fbb0a3329/nuxt-installation-options-in-terminal.png"
			
			sizes="100vw"
			alt="Nuxt installation options in terminal"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Selected options for Nuxt. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/73cc1fb3-0424-4760-a512-9f9fbb0a3329/nuxt-installation-options-in-terminal.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>After successfully installing the Nuxt app, navigate to the directory by running this command:</p>

<pre><code class="language-bash">cd algolia-nuxt</code></pre>

<h2 id="install-nuxt-content-separately">Install Nuxt Content Separately</h2>

<p>If you already have Nuxt set up before now, you can install the content module by running the command.</p>

<p><em>Skip this if you’ve already selected to install the <code>nuxt/content</code> module along with our Nuxt app.</em></p>

<pre><code class="language-bash">#install nuxt content

npm install @nuxt/content</code></pre>

<p>Then you can add it to our <code>modules</code> property inside our <code>nuxt.config</code> file.</p>

<pre><code class="language-javascript">//nuxt.config.js

export default {
  modules: ['@nuxt/content']
}</code></pre>

<h2 id="install-and-setup-tailwindcss">Install And Setup TailwindCSS</h2>

<p><a href="https://tailwindcss.com/docs/guides/nuxtjs">TailwindCSS</a> is a utility first CSS framework that provides us with custom classes we can use to style our app.</p>

<p>We’ll also be using <a href="https://github.com/tailwindlabs/tailwindcss-typography">TailwindCSS Typography</a>, which is &ldquo;a plugin that provides a set of <code>prose</code> classes you can use to add beautiful typographic defaults to any vanilla HTML you don’t control (like HTML rendered from Markdown, or pulled from a CMS).&rdquo;</p>

<p>First, we install <code>@nuxtjs/tailwindcss</code> which is a Nuxt module for TailwindCSS integration, as well as TailwindCSS and its peer-dependencies using npm:</p>

<pre><code class="language-bash">npm install -D @nuxtjs/tailwindcss tailwindcss@latest postcss@latest autoprefixer@latest</code></pre>

<p>Add the <code>@nuxtjs/tailwindcss</code> module to the <code>buildModules</code> section of our nuxt.config.js file:</p>

<pre><code class="language-javascript">// nuxt.config.js

export default {
  buildModules: ['@nuxtjs/tailwindcss']
}</code></pre>

<h3 id="create-configuration-file">Create Configuration File</h3>

<p>Next, generate our <code>tailwind.config.js</code> file:</p>

<pre><code class="language-javascript">npx tailwindcss init</code></pre>

<p>This will create a minimal <code>tailwind.config.js</code> file at the root of  our project:</p>

<pre><code class="language-javascript">//tailwind.config.js

module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}</code></pre>

<p>Create a <code>tailwind.css</code> file in <code>assets/css/</code> use the <code>@tailwind</code> directive to inject TailwindCSS’ base, components, and utilities styles:</p>

<pre><code class="language-css">/*assets/css/tailwind.css*/

@tailwind base;
@tailwind components;
@tailwind utilities;</code></pre>

<p>You can import the CSS file into  our components or make it accessible globally by defining the CSS files/modules/libraries you want to set globally <em>(included in every page).</em></p>

<pre><code class="language-javascript">  /* nuxt.config.js*/

  // Global CSS: https://go.nuxtjs.dev/config-css
  css: [
    // CSS file in the project
    '@/assets/css/tailwind.css',
  ],</code></pre>

<p>Here, we have added the path to our <code>tailwind.css</code> file to the list of global CSS files in our <code>nuxt.config.js</code>.</p>

<blockquote>
<p>&ldquo;The <code>@/</code> tells Nuxt that it&rsquo;s an absolute path to look for the file from the root directory.&rdquo;</p>
</blockquote>

<h3 id="install-tailwindcss-typography">Install TailwindCSS Typography</h3>

<pre><code class="language-bash"># Using npm
npm install @tailwindcss/typography</code></pre>

<p>Then add the plugin to our <code>tailwind.config.js</code> file:</p>

<pre><code class="language-javascript">// tailwind.config.js
module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/typography'),
  ],
}</code></pre>

<h3 id="configure-tailwindcss-to-remove-unused-styles-in-production">Configure TailwindCSS To Remove Unused Styles In Production</h3>

<p>In our <code>tailwind.config.js</code> file, configure the purge option with the paths to all of our pages and components so TailwindCSS can tree-shake unused styles in production builds:</p>

<pre><code class="language-javascript">// tailwind.config.js
module.exports = {
  purge: [
    './components/**/*.{vue,js}',
    './layouts/**/*.vue',
    './pages/**/*.vue',
    './plugins/**/*.{js,ts}',
    './nuxt.config.{js,ts}',
  ],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/typography'),
  ],
}</code></pre>

<p>Since we’ve installed the packages, let’s start our app:</p>

<pre><code class="language-bash">npm run dev</code></pre>

<p>This command starts our Nuxt app in development mode.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27b671bb-e8f6-47e2-92a2-3ba9dc260b79/screenshot-of-default-nuxt-page.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="407"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27b671bb-e8f6-47e2-92a2-3ba9dc260b79/screenshot-of-default-nuxt-page.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27b671bb-e8f6-47e2-92a2-3ba9dc260b79/screenshot-of-default-nuxt-page.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27b671bb-e8f6-47e2-92a2-3ba9dc260b79/screenshot-of-default-nuxt-page.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27b671bb-e8f6-47e2-92a2-3ba9dc260b79/screenshot-of-default-nuxt-page.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27b671bb-e8f6-47e2-92a2-3ba9dc260b79/screenshot-of-default-nuxt-page.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27b671bb-e8f6-47e2-92a2-3ba9dc260b79/screenshot-of-default-nuxt-page.png"
			
			sizes="100vw"
			alt="Screenshot of default Nuxt page"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Nuxt app started. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/27b671bb-e8f6-47e2-92a2-3ba9dc260b79/screenshot-of-default-nuxt-page.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Nice 🍻</p>

<h2 id="creating-our-pages-and-articles">Creating Our Pages And Articles</h2>

<p>Now, let’s create our articles and a blog page to list out our articles. But first, let’s create a site header and navigation component for our site.</p>

<h3 id="creating-a-site-header-and-navigation">Creating A Site Header And Navigation</h3>

<p>Navigate to our <code>components/</code>folder, and create a new file <code>siteHeader.vue</code> and enter the following code:</p>

<pre><code class="language-javascript">&lt;!-- components/siteHeader.vue --&gt;

&lt;template&gt;
  &lt;header class="fixed top-0 w-full bg-white bg-opacity-90 backdrop-filter backdrop-blur-md"&gt;
    &lt;div class="wrapper flex items-center justify-between p-4 m-auto max-w-5xl"&gt;
      &lt;nuxt-link to="/"&gt;
        &lt;Logo /&gt;
      &lt;/nuxt-link&gt;

      &lt;nav class="site-nav"&gt;
        &lt;ul class="links"&gt;
          &lt;li&gt;
            &lt;nuxt-link to="/blog"&gt;Blog&lt;/nuxt-link&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/nav&gt;
    &lt;/div&gt;
  &lt;/header&gt;
&lt;/template&gt;</code></pre>

<p>Here, in our <code>&lt;header&gt;</code> we have a <code>&lt;Logo /&gt;</code> component wrapped in <code>&lt;nuxt-link&gt;</code> which routes to the home page and another <code>&lt;nuxt-link&gt;</code> that routes to <code>/blog</code> <em>(We’ll create the blog page that we will create later on)</em>.</p>

<p>This works without us importing the components and configuring routing ourselves because, by default, Nuxt handles importing components and routing for us.</p>

<p>Also, let’s modify the default <code>&lt;Logo /&gt;</code> component. In <code>components/Logo.vue</code>, replace the content with the following code:</p>

<pre><code class="language-javascript">&lt;!-- components/Logo.vue --&gt;

&lt;template&gt;
  &lt;figure class="site-logo text-2xl font-black inline-block"&gt;
    &lt;h1&gt;Algolia-nuxt&lt;/h1&gt;
  &lt;/figure&gt;
&lt;/template&gt;</code></pre>

<p>We can now add our <code>siteHeader.vue</code> component to our site. In <code>layouts/default.vue</code>, add <code>&lt;site-header /&gt;</code> just above the <code>&lt;Nuxt /&gt;</code> component.</p>

<pre><code class="language-javascript">&lt;!-- layouts/default.vue --&gt;

&lt;template&gt;
  &lt;div&gt;
    &lt;site-header /&gt;
    &lt;Nuxt /&gt;
  &lt;/div&gt;
&lt;/template&gt;

...</code></pre>

<p>The <code>&lt;Nuxt /&gt;</code> component renders the current Nuxt page depending on the route.</p>

<h3 id="creating-our-first-article">Creating Our First Article</h3>

<p>In <code>content/</code>, which is a folder created automatically for the <code>nuxt/content</code> module, create a new folder <code>articles/</code> and then a new file in the folder <code>first-blog-post.md</code>. Here is the file for our first article in <code>markdown</code> format. Enter the following code:</p>

<pre><code class="language-html">&lt;!-- content/articles/first-blog-post.md --&gt;

---

title: My first blog post
description: This is my first blog post on algolia nuxt
tags: [first, lorem ipsum, Iusto]

---

## Lorem ipsum

Lorem ipsum dolor sit amet consectetur, adipisicing elit.
Assumenda dolor quisquam consequatur distinctio perferendis.

## Iusto nobis nisi

repellat magni facilis necessitatibus, enim temporibus.

- Quisquam
- assumenda
- sapiente explicabo
- totam nostrum inventore</code></pre>

<p>The area enclosed with <code>---</code> is the <code>YAML</code> Front Matter which will be used as a custom injected variable that we will access in our template.</p>

<p>Next, we’re going to create a <a href="https://nuxtjs.org/docs/2.x/directory-structure/pages#dynamic-pages">dynamic page</a> which will be used to:</p>

<ul>
<li>Fetch the article content using <code>asyncData</code> which runs before the page has been rendered. We have access to our content and custom injected variables through the context by using the variable <code>$content</code>. As we are using a dynamic page, we can know what article file to fetch using the <code>params.slug</code> variable provided by <a href="https://router.vuejs.org/">Vue Router</a> to get the name of each article.</li>
<li>Render the article in the template using <code>&lt;nuxt-content&gt;</code>.</li>
</ul>

<p>Ok, navigate to <code>pages/</code> and create a <code>blog/</code> folder. Create a <code>_slug.vue</code> (our dynamic page) file and insert the following:</p>

<pre><code class="language-javascript">&lt;!-- pages/blog/_slug.vue --&gt;

&lt;template&gt;
  &lt;article class="prose prose-lg lg:prose-xl p-4 mt-24 m-auto max-w-4xl"&gt;
    &lt;header&gt;
      &lt;h1&gt;{{ article.title }}&lt;/h1&gt;
      &lt;p&gt;{{ article.description }}&lt;/p&gt;
      &lt;ul class="list-none"&gt;
        &lt;li class="inline-block mr-2 font-bold font-monospace" v-for="tag in article.tags" :key="tag" &gt; {{tag}} &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/header&gt;
    &lt;!-- this is where we will render the article contents --&gt;
    &lt;nuxt-content :document="article" /&gt;
  &lt;/article&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  async asyncData({ $content, params }) {
    //here, we will fetch the article from the articles/ folder using the name provided in the `params.slug`
    const article = await $content('articles', params.slug).fetch()

    //return `article` which contains our custom injected variables and the content of our article
    return { article }
  },
}
&lt;/script&gt;</code></pre>

<p>If you go to your browser and navigate to <code>https://localhost:3000/blog/first-blog-post</code> you should see our rendered content:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f853824e-400d-4a70-9401-b323322f50c6/screenshot-of-rendered-article-page.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="406"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f853824e-400d-4a70-9401-b323322f50c6/screenshot-of-rendered-article-page.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f853824e-400d-4a70-9401-b323322f50c6/screenshot-of-rendered-article-page.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f853824e-400d-4a70-9401-b323322f50c6/screenshot-of-rendered-article-page.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f853824e-400d-4a70-9401-b323322f50c6/screenshot-of-rendered-article-page.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f853824e-400d-4a70-9401-b323322f50c6/screenshot-of-rendered-article-page.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f853824e-400d-4a70-9401-b323322f50c6/screenshot-of-rendered-article-page.png"
			
			sizes="100vw"
			alt="Screenshot of rendered article page"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Article page rendered. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f853824e-400d-4a70-9401-b323322f50c6/screenshot-of-rendered-article-page.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Now that our dynamic page is working and our article is rendering, let’s create some duplicates for the purpose of this tutorial.</p>

<pre><code class="language-html">&lt;!-- content/articles/second-blog-post.md --&gt;

---

title: My first blog post
description: This is my first blog post on algolia nuxt
tags: [first, Placeat amet, Iusto]

---

## Lorem ipsum

Lorem ipsum dolor sit amet consectetur, adipisicing elit.
Assumenda dolor quisquam consequatur distinctio perferendis.

## Iusto nobis nisi

repellat magni facilis necessitatibus, enim temporibus.

- Quisquam
- assumenda
- sapiente explicabo
- totam nostrum inventore</code></pre>

<h3 id="create-blog-page-to-list-our-articles">Create Blog Page To List Our Articles</h3>

<p>Let’s now create a blog page to list our articles. This is also where our search bar will live. Create a new file <code>pages/blog/index.vue</code>.</p>

<pre><code class="language-javascript">&lt;!-- pages/blog/index.vue --&gt;

&lt;template&gt;
  &lt;main&gt;
    &lt;section class="p-4 mt-24 m-auto max-w-4xl"&gt;
      &lt;header&gt;
        &lt;h1 class="font-black text-2xl"&gt;All posts&lt;/h1&gt;
          
        &lt;!-- dummy search bar --&gt;
        &lt;div class="search-cont inline-flex gap-2 bg-white p-2 rounded-lg shadow-lg"&gt;
          &lt;input class="px-2 outline-none" type="search" name="search" id="search"&gt;
          &lt;button class="bg-blue-600 text-white px-2 rounded-md" type="submit"&gt;Search&lt;/button&gt;
        &lt;/div&gt;
      &lt;/header&gt;
      &lt;ul class="prose prose-xl"&gt;
          &lt;!-- list out all fetched articles --&gt; 
        &lt;li v-for="article in articles" :key="article.slug"&gt;
          &lt;nuxt-link :to="{ name: 'blog-slug', params: { slug: article.slug } }"&gt;
            &lt;h2 class="mb-0"&gt;{{ article.title }}&lt;/h2&gt;
            &lt;p class="mt-0"&gt;{{ article.description }}&lt;/p&gt;
          &lt;/nuxt-link&gt;
        &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/section&gt;
  &lt;/main&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  async asyncData({ $content }) {
    // fetch all articles in the folder and return the:
    const articles = await $content('articles')
      // title, slug and description
      .only(['title', 'slug', 'description'])
      // sort the list by the `createdAt` time in `ascending order`
      .sortBy('createdAt', 'asc')
      .fetch()

    return { articles }
  },
}
&lt;/script&gt;</code></pre>

<p>Here, in our <code>asyncData</code> function, when fetching <code>$content('articles')</code> we chain <code>.only(['title', 'slug', 'updatedAt', 'description'])</code> to fetch only those attributes from the articles, <code>.sortBy('createdAt', 'asc')</code> to sort it and lastly <code>fetch()</code> to fetch the data and assign it to <code>const articles</code> which we then return.</p>

<p>So, in our <code>&lt;template&gt;</code>, we can the list of articles and create links to them using their <code>slug</code> property.</p>

<p>Our page should look something like this:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/1cbbe4a5-bb8f-4576-b50f-1bd8d57557a4/screenshot-of-blog-page-listing-all-articles.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="408"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/1cbbe4a5-bb8f-4576-b50f-1bd8d57557a4/screenshot-of-blog-page-listing-all-articles.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/1cbbe4a5-bb8f-4576-b50f-1bd8d57557a4/screenshot-of-blog-page-listing-all-articles.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/1cbbe4a5-bb8f-4576-b50f-1bd8d57557a4/screenshot-of-blog-page-listing-all-articles.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/1cbbe4a5-bb8f-4576-b50f-1bd8d57557a4/screenshot-of-blog-page-listing-all-articles.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/1cbbe4a5-bb8f-4576-b50f-1bd8d57557a4/screenshot-of-blog-page-listing-all-articles.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/1cbbe4a5-bb8f-4576-b50f-1bd8d57557a4/screenshot-of-blog-page-listing-all-articles.png"
			
			sizes="100vw"
			alt="Screenshot of Blog page listing all articles"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Blog page listing all articles. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/1cbbe4a5-bb8f-4576-b50f-1bd8d57557a4/screenshot-of-blog-page-listing-all-articles.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Great 🍻</p>

<h2 id="install-and-set-up-algolia-search-and-vue-instantsearch">Install And Set Up Algolia Search And Vue-instantSearch</h2>

<p>Now that we’ve gotten the basic stuff out of the way, we can integrate Algolia Search into our blog site.</p>

<p>First, let’s install all the packages we will be needing:</p>

<pre><code class="language-bash">#install dependencies

npm install vue-instantsearch instantsearch.css algoliasearch nuxt-content-algolia remove-markdown dotenv</code></pre>

<ul>
<li><strong><code>vue-instantsearch</code></strong><br />
Algolia InstantSearch UI component/widget library for Vue.</li>
<li><strong><code>instantsearch.css</code></strong><br />
Custom styling for instantSearch widgets.</li>
<li><strong><code>algoliasearch</code></strong><br />
A HTTP client to interact with Algolia.</li>
<li><strong><code>nuxt-content-algolia</code></strong><br />
Package for indexing our content and sending it to Algolia.</li>
<li><strong><code>remove-markdown</code></strong><br />
This strips all markdown characters from the <code>bodyPlainText</code> of the articles.</li>
<li><strong><code>dotenv</code></strong><br />
This helps to read environment variables from <code>.env</code> files.</li>
</ul>

<p>We’ll be using these packages throughout the rest of this tutorial, but first, let’s set up an Algolia account.</p>

<h3 id="set-up-algolia-account">Set Up Algolia Account</h3>

<p>Sign up for an Algolia account at <a href="https://www.algolia.com/">https://www.algolia.com/</a>. You can do this for free, however, this will give you a trial period of 14days. Since we’re not performing heavy tasks with Algolia, their free tier will do just fine for our project after the trial expires.</p>

<p>You’ll be taken through some onboarding steps. After that, an <em>UNAMED APP</em> will be created for you. On the sidebar, on the left, navigate to <strong>the API Keys</strong> you’ll be provided with:</p>

<ul>
<li><strong>Application ID</strong><br />
This is your unique application identifier. It’s used to identify you when using Algolia’s API.</li>
<li><strong>Search Only API Key</strong><br />
This is the public API key to use in your frontend code. This key is only usable for search queries and sending data to the Insights API.</li>
<li><strong>Admin API Key</strong><br />
This key is used to create, update and DELETE your indices. You can also use it to manage your API keys.</li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bbb4962e-7f60-4404-a08a-e2fe58610a6a/screenshot-of-algolia-account-page-to-get-api-keys.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bbb4962e-7f60-4404-a08a-e2fe58610a6a/screenshot-of-algolia-account-page-to-get-api-keys.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bbb4962e-7f60-4404-a08a-e2fe58610a6a/screenshot-of-algolia-account-page-to-get-api-keys.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bbb4962e-7f60-4404-a08a-e2fe58610a6a/screenshot-of-algolia-account-page-to-get-api-keys.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bbb4962e-7f60-4404-a08a-e2fe58610a6a/screenshot-of-algolia-account-page-to-get-api-keys.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bbb4962e-7f60-4404-a08a-e2fe58610a6a/screenshot-of-algolia-account-page-to-get-api-keys.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bbb4962e-7f60-4404-a08a-e2fe58610a6a/screenshot-of-algolia-account-page-to-get-api-keys.png"
			
			sizes="100vw"
			alt="Screenshot of Algolia account page to get API Keys"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Algolia account to retrieve API keys. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bbb4962e-7f60-4404-a08a-e2fe58610a6a/screenshot-of-algolia-account-page-to-get-api-keys.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Now that we have our API Keys, let’s save them in an <code>.env</code> file for our project. Navigate to the project root folder and create a new file <code>.env</code> and enter your API keys:</p>

<pre><code class="language-javascript">.env

ALGOLIA_APP_ID=algolia-app-id
ALGOLIA_API_KEY=algolia-admin-api-key</code></pre>

<p>Replace <code>algolia-app-id</code> and <code>algolia-admin-api-key</code> with your Application ID and Admin API Key respectively.</p>

<h3 id="create-an-articles-index-for-our-nuxt-articles-in-algolia">Create An <code>'Articles'</code> Index For Our Nuxt Articles In Algolia</h3>

<p>On your Algolia account, go to <strong>Indices</strong> and click on <strong>create Index</strong>. Then enter the name of your index and we’ll be using <strong>articles</strong> for this tutorial.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b37bf5fd-b7d0-4cc0-b1e5-a43e42ecc216/screenshot-of-algolia-account-page-to-create-new-index.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b37bf5fd-b7d0-4cc0-b1e5-a43e42ecc216/screenshot-of-algolia-account-page-to-create-new-index.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b37bf5fd-b7d0-4cc0-b1e5-a43e42ecc216/screenshot-of-algolia-account-page-to-create-new-index.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b37bf5fd-b7d0-4cc0-b1e5-a43e42ecc216/screenshot-of-algolia-account-page-to-create-new-index.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b37bf5fd-b7d0-4cc0-b1e5-a43e42ecc216/screenshot-of-algolia-account-page-to-create-new-index.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b37bf5fd-b7d0-4cc0-b1e5-a43e42ecc216/screenshot-of-algolia-account-page-to-create-new-index.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b37bf5fd-b7d0-4cc0-b1e5-a43e42ecc216/screenshot-of-algolia-account-page-to-create-new-index.png"
			
			sizes="100vw"
			alt="Screenshot of Algolia account page to create new index"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Create a new <code>'articles'</code> index on Algolia. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b37bf5fd-b7d0-4cc0-b1e5-a43e42ecc216/screenshot-of-algolia-account-page-to-create-new-index.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>As you can see, our <code>'article'</code> index has been created.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c756c99-d45a-4765-a2c0-8347f1ff5955/screenshot-of-algolia-account-page-created-index.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c756c99-d45a-4765-a2c0-8347f1ff5955/screenshot-of-algolia-account-page-created-index.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c756c99-d45a-4765-a2c0-8347f1ff5955/screenshot-of-algolia-account-page-created-index.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c756c99-d45a-4765-a2c0-8347f1ff5955/screenshot-of-algolia-account-page-created-index.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c756c99-d45a-4765-a2c0-8347f1ff5955/screenshot-of-algolia-account-page-created-index.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c756c99-d45a-4765-a2c0-8347f1ff5955/screenshot-of-algolia-account-page-created-index.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c756c99-d45a-4765-a2c0-8347f1ff5955/screenshot-of-algolia-account-page-created-index.png"
			
			sizes="100vw"
			alt="Screenshot of Algolia account page, created index"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      New <code>'articles'</code> index created. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c756c99-d45a-4765-a2c0-8347f1ff5955/screenshot-of-algolia-account-page-created-index.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="set-up-nuxt-content-algolia-to-send-content-index-to-algolia">Set Up <code>nuxt-content-algolia</code> To Send Content Index To Algolia</h3>

<p>We’ve successfully created an index property on our account. Now we have to generate an index from our Nuxt articles which is what Algolia will use to provide results for search queries. This is what the <code>nuxt-content-algolia</code> module that we previously installed is for.</p>

<p>We need to configure it in our <code>nuxt.config.js</code>.</p>

<p>First, we will add it to our <code>buildModules</code>:</p>

<pre><code class="language-javascript">// nuxt.config.js

...

// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: ['@nuxtjs/tailwindcss', 'nuxt-content-algolia'],

...</code></pre>

<p>Then, we create a new <code>nuxtContentAlgolia</code> object and add a few configurations to it:</p>

<pre><code class="language-javascript">// nuxt.config.js

export default {
...

nuxtContentAlgolia: {

  // Application ID
  appId: process.env.ALGOLIA_APP_ID,
    
  // Admin API Key
  // !IMPORTANT secret key should always be an environment variable
  // this is not your search only key but the key that grants access to modify the index
  apiKey: process.env.ALGOLIA_ADMIN_API_KEY,

  paths: [
    {
      name: 'articles',
      index: process.env.ALGOLIA_INDEX || 'articles',
      fields: ['title', 'description', 'tags', 'bodyPlainText']
    }
  ]
},


...
}</code></pre>

<p>The <code>nuxtContentAlgolia</code> takes in the following properties:</p>

<ul>
<li><strong><code>appId</code></strong><br />
Application ID*.</li>
<li><strong><code>apiKey</code></strong><br />
Admin API Key.</li>
<li><strong><code>paths</code></strong><br />
An array of index objects. This is where we define where we want to generate indexes from. Each object takes the following properties:

<ul>
<li><strong><code>name</code></strong><br />
The name of the folder within the <code>content/</code> folder. In other words, we’ll be using files within <code>content/articles/</code> since we defined the name as <code>'articles'</code>.</li>
<li><strong><code>index</code></strong><br />
This is the name of the index we created on our Algolia dashboard.</li>
<li><strong><code>fields</code></strong><br />
An array of fields to be indexed. This is what Algolia will base its search queries on.</li>
</ul></li>
</ul>

<h3 id="generate-bodyplaintext-from-articles">Generate <code>bodyPlainText</code> From Articles</h3>

<p>Note that in the <code>fields</code> array, we have <code>bodyPlainText</code> as one of its values. Nuxt Content does not provide such a field for us. Instead, what Nuxt Content provides is <code>body</code> which is a complex object that will be rendered in the DOM.</p>

<p>In order to get our <code>bodyPlainText</code> which is simply all text, stripped of markdown and HTML characters, we have to make use of yet another package, <code>remove-markdown</code>.</p>

<p>To use the <code>remove-markdown</code> function we need to make use of Nuxt <code>hooks</code>. We’ll use the <code>'content:file:beforeInsert'</code> hook which allows you to add data to a document before it is inserted, to strip off the markdown and add the generated plain text to <code>bodyPlainText</code>.</p>

<pre><code class="language-javascript">// nuxt.config.js

export default {
...
    
hooks: {
  'content:file:beforeInsert': (document)=&gt;{
    const removeMd = require('remove-markdown');

    if(document.extension === '.md'){
      document.bodyPlainText = removeMd(document.text);
    }
  }
},

...
}</code></pre>

<p>In the <code>'content:file:beforeInsert'</code> hook, we get the <code>remove-markdown</code> package. Then we check if the file to be inserted is a markdown file. If it is a markdown file, we generate the plain text by calling <code>removeMd</code> which takes <code>document.text</code> — the text of our content, as an argument, which we assign to a new <code>document.bodyPlainText</code> property. The property will now be available for use through Nuxt Content.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/40151123-4f97-4e7f-b2ea-91337d7ff8e9/vue-devtools-showing-bodyplaintext-property.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/40151123-4f97-4e7f-b2ea-91337d7ff8e9/vue-devtools-showing-bodyplaintext-property.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/40151123-4f97-4e7f-b2ea-91337d7ff8e9/vue-devtools-showing-bodyplaintext-property.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/40151123-4f97-4e7f-b2ea-91337d7ff8e9/vue-devtools-showing-bodyplaintext-property.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/40151123-4f97-4e7f-b2ea-91337d7ff8e9/vue-devtools-showing-bodyplaintext-property.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/40151123-4f97-4e7f-b2ea-91337d7ff8e9/vue-devtools-showing-bodyplaintext-property.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/40151123-4f97-4e7f-b2ea-91337d7ff8e9/vue-devtools-showing-bodyplaintext-property.png"
			
			sizes="100vw"
			alt="Vue Devtools showing bodyPlainText property"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      <code>BodyPlainText</code> generated and visible in Nuxt. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/40151123-4f97-4e7f-b2ea-91337d7ff8e9/vue-devtools-showing-bodyplaintext-property.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Great! Now that that’s done, we can generate the index and send it over to Algolia.</p>

<h3 id="confirm-algolia-index">Confirm Algolia Index</h3>

<p>Alright. We’ve set up <code>nuxt-content-algolia</code> and we’ve generated <code>bodyPlainText</code> for our articles. We can now generate this index and send the data over to Algolia by building our project using <code>nuxt generate</code>.</p>

<pre><code class="language-bash">npm run generate</code></pre>

<p>This will start building our project for production and run the <code>nuxtContentAlgolia</code> config. When we look at our terminal after the build we should see that our content has been indexed and sent to Algolia.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/4c26c53c-7376-460b-b4c5-da995bbab5f0/screenshot-of-terminal-showing-indexed-2-records.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="303"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/4c26c53c-7376-460b-b4c5-da995bbab5f0/screenshot-of-terminal-showing-indexed-2-records.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/4c26c53c-7376-460b-b4c5-da995bbab5f0/screenshot-of-terminal-showing-indexed-2-records.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/4c26c53c-7376-460b-b4c5-da995bbab5f0/screenshot-of-terminal-showing-indexed-2-records.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/4c26c53c-7376-460b-b4c5-da995bbab5f0/screenshot-of-terminal-showing-indexed-2-records.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/4c26c53c-7376-460b-b4c5-da995bbab5f0/screenshot-of-terminal-showing-indexed-2-records.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/4c26c53c-7376-460b-b4c5-da995bbab5f0/screenshot-of-terminal-showing-indexed-2-records.png"
			
			sizes="100vw"
			alt="Screenshot of terminal showing Indexed 2 records..."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Index generated for articles. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/4c26c53c-7376-460b-b4c5-da995bbab5f0/screenshot-of-terminal-showing-indexed-2-records.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To verify, you can go to your Algolia dashboard:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8fae46fe-d907-4d8f-87b2-b8fb0b2ba28e/screenshot-of-algolia-indices-page-showing-search-api-logs-to-confirm-index-creation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8fae46fe-d907-4d8f-87b2-b8fb0b2ba28e/screenshot-of-algolia-indices-page-showing-search-api-logs-to-confirm-index-creation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8fae46fe-d907-4d8f-87b2-b8fb0b2ba28e/screenshot-of-algolia-indices-page-showing-search-api-logs-to-confirm-index-creation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8fae46fe-d907-4d8f-87b2-b8fb0b2ba28e/screenshot-of-algolia-indices-page-showing-search-api-logs-to-confirm-index-creation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8fae46fe-d907-4d8f-87b2-b8fb0b2ba28e/screenshot-of-algolia-indices-page-showing-search-api-logs-to-confirm-index-creation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8fae46fe-d907-4d8f-87b2-b8fb0b2ba28e/screenshot-of-algolia-indices-page-showing-search-api-logs-to-confirm-index-creation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8fae46fe-d907-4d8f-87b2-b8fb0b2ba28e/screenshot-of-algolia-indices-page-showing-search-api-logs-to-confirm-index-creation.png"
			
			sizes="100vw"
			alt="Screenshot of Algolia indices page showing search api logs to confirm index creation"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Confirm index in Algolia Search API logs. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8fae46fe-d907-4d8f-87b2-b8fb0b2ba28e/screenshot-of-algolia-indices-page-showing-search-api-logs-to-confirm-index-creation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Open <em>Indices</em>, then go to <em>Search API logs</em>, where you will see a log of operations performed with your <em>Search API</em>. You can now open and check the API call sent from your Nuxt project. This should have the content of your article as specified in the <code>fields</code> section of <code>nuxtContentAlgolia</code> config.</p>

<p>Nice! 🍻</p>

<div class="partners__lead-place"></div>

<h2 id="building-the-search-ui">Building The Search UI</h2>

<p>So far we’ve been able to generate and send index data to Algolia, which means that we are able to query this data to get search results.</p>

<p>To do that within our app, we have to build our search UI.</p>

<p><code>Vue-InstantSearch</code> provides lots of UI components using Algolia that can be integrated to provide a rich search experience for users. Let’s set it up.</p>

<h3 id="create-and-configure-vue-instantsearch-plugin">Create And Configure <code>vue-instantSearch</code> Plugin</h3>

<p>In order to use the Algolia <code>InstantSearch</code> widgets in our Nuxt app, we will have to create a plugin in our <code>plugins</code> folder.</p>

<p>Go to <code>plugins/</code> and create a new file <code>vue-instantsearch.js</code>.</p>

<pre><code class="language-javascript">// plugins/vue-instantsearch.js

import Vue from 'vue'
import InstantSearch from 'vue-instantsearch'

Vue.use(InstantSearch)</code></pre>

<p>Here, we’re simply importing <code>InstantSearch</code> and using it on the <code>Vue</code> frontend.</p>

<p>Now, we have to add the <code>vue-instantSearch</code> plugin to our plugins and build options in <code>nuxt.config.js</code> in order to transpile it to Vue.js.</p>

<p>So, go over to <code>nuxt.config.js</code> and add the following:</p>

<pre><code class="language-javascript">// nuxt.config.js

export default {
...

// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: ['@/plugins/vue-instantsearch.js'],

// Build Configuration: https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-build#transpile
build: {
  transpile: ['vue-instantsearch', 'instantsearch.js/es']
}

...
}</code></pre>

<p><code>InstantSearch</code> code uses ES modules, yet it needs to be executed in <code>Node.js</code>. That’s why we need to let Nuxt know that those files should be transpiled during the build.
Now that we’ve configured our <code>vue-instantSearch</code> plugin, let’s create a search component.</p>

<h3 id="create-a-search-component">Create A Search Component</h3>

<p>Create a new file <code>components/Search.vue</code>.</p>

<p>Since we’ve installed <code>vue-instantSearch</code> as a plugin, we can use it within our Vue components.</p>

<pre><code class="language-javascript">&lt;!-- components/Search.vue --&gt;

...

&lt;script&gt;
import algoliaSearch from 'algoliasearch/lite'
import 'instantsearch.css/themes/satellite-min.css'

// configurations for Algolia search
const searchClient = algoliaSearch(
  // Applictaion ID
  '34IIDW6KKR',
    
  // Search API key
  '3f8d80be6c42bb030d27a7f108eb75f8'
)
export default {
    data(){
        return{
            searchClient
        }
    }
}
&lt;/script&gt;</code></pre>

<p>First, in the <code>&lt;script&gt;</code> section, we’re importing <code>algoliaSearch</code> and <code>instantsearch.css</code>.</p>

<p>Next, we provide the credentials for our Algolia search which are:</p>

<ul>
<li><strong>Application ID</strong>,</li>
<li><strong>Search API key</strong>.</li>
</ul>

<p>As parameters to <code>algoliaSearch</code> then assign it to <code>searchClient</code> which we will use in our <code>&lt;template&gt;</code> to configure our Algolia search widgets.</p>

<h3 id="ais-instant-search-widget"><code>ais-instant-search</code> Widget</h3>

<p><code>ais-instant-search</code> is the root Vue <code>InstantSearch</code> component. All other widgets need to be wrapped with the root component to function. The required attributes for this component are:</p>

<ul>
<li><strong><code>index-name</code></strong><br />
Name of the index to query, in this case, it would be <code>articles</code>.</li>
<li><strong><code>search-client</code></strong><br />
<code>algoliaSearch</code> object containing Application ID and Search API Key.</li>
</ul>

<pre><code class="language-javascript">&lt;!-- components/Search.vue --&gt;

&lt;template&gt;
  &lt;div class="search-cont inline-flex gap-2 bg-white p-2 rounded-lg shadow-lg"&gt;
    &lt;ais-instant-search index-name="articles" :search-client="searchClient"&gt;
    &lt;/ais-instant-search&gt;
  &lt;/div&gt;
&lt;/template&gt;

...</code></pre>

<h3 id="ais-configure-widget"><code>ais-configure</code> Widget</h3>

<p>The <code>ais-configure</code> widget helps configure the search functionality by sending defined parameters to Algolia.</p>

<p>Any props you add to this widget will be forwarded to Algolia. For more information on the different parameters you can set, have a look at the <a href="https://www.algolia.com/doc/api-reference/search-api-parameters/">search parameters API reference</a>.</p>

<p>The parameters we’ll set for now will be:</p>

<ul>
<li><strong><code>attributesToSnippet</code></strong><br />
The name of the attribute or <code>field</code> to snippet in, we’ll soon see more on this.</li>
<li><strong><code>hits-per-page.camel</code></strong><br />
Number of results in one page.</li>
<li><strong><code>snippetEllipsisText=&quot;…&quot;</code></strong><br />
Set <code>...</code> before and after snipped text.</li>
</ul>

<pre><code class="language-javascript">&lt;!-- components/Search.vue --&gt;

&lt;template&gt;
  &lt;div class="search-cont inline-flex gap-2 bg-white p-2 rounded-lg shadow-lg"&gt;
    &lt;ais-instant-search index-name="articles" :search-client="searchClient"&gt;
      &lt;ais-configure
        :attributesToSnippet="['bodyPlainText']"
        :hits-per-page.camel="5"
        snippetEllipsisText="…"
      &gt;
      &lt;/ais-configure&gt;
    &lt;/ais-instant-search&gt;
  &lt;/div&gt;
&lt;/template&gt;

...</code></pre>

<h3 id="ais-autocomplete-widget"><code>ais-autocomplete</code> Widget</h3>

<p>This widget is basically a wrapper that allows us to create a search result that autocompletes the query. Within this widget, we can connect to other widgets to provide a richer UI and access multiple indices.</p>

<pre><code class="language-javascript">&lt;!-- components/Search.vue --&gt;

&lt;template&gt;
  &lt;div class="search-cont inline-flex gap-2 bg-white p-2 rounded-lg shadow-lg"&gt;
    &lt;ais-instant-search index-name="articles" :search-client="searchClient"&gt;
      &lt;ais-configure
        :attributesToSnippet="['bodyPlainText']"
        :hits-per-page.camel="5"
        snippetEllipsisText="…"
      &gt;
        &lt;ais-autocomplete&gt;
          &lt;template v-slot="{ currentRefinement, indices, refine }"&gt;
            &lt;input
              type="search"
              :value="currentRefinement"
              placeholder="Search for an article"
              @input="refine($event.currentTarget.value)"
            /&gt;
            &lt;ais-stats /&gt;
            &lt;template v-if="currentRefinement"&gt;
              &lt;ul v-for="index in indices" :key="index.indexId"&gt;
                &lt;li&gt;
                  &lt;h3&gt;{{ index.indexName }}&lt;/h3&gt;
                  &lt;ul&gt;
                    &lt;li v-for="hit in index.hits" :key="hit.objectID"&gt;
                      &lt;h1&gt;
                        &lt;ais-highlight attribute="title" :hit="hit" /&gt;
                      &lt;/h1&gt;
                      &lt;h2&gt;
                        &lt;ais-highlight attribute="description" :hit="hit" /&gt;
                      &lt;/h2&gt;
                      &lt;p&gt;
                        &lt;ais-snippet attribute="bodyPlainText" :hit="hit" /&gt;
                      &lt;/p&gt;
                    &lt;/li&gt;
                  &lt;/ul&gt;
                &lt;/li&gt;
              &lt;/ul&gt;
            &lt;/template&gt;
            &lt;ais-pagination /&gt;
          &lt;/template&gt;
        &lt;/ais-autocomplete&gt;
      &lt;/ais-configure&gt;
    &lt;/ais-instant-search&gt;
  &lt;/div&gt;
&lt;/template&gt;

...</code></pre>

<p>So, within our <code>ais-autocomplete</code> widget, we’re doing a few things:</p>

<ul>
<li>Overriding the  DOM output of the widget using the <code>default</code> slot. We’re doing this using the scopes:

<ul>
<li><strong><code>currentRefinement: string</code></strong>: the current value of the query.</li>
<li><strong><code>indices: object[]</code></strong>: the list of indices.</li>
<li><strong><code>refine: (string) =&gt; void</code></strong>: the function to change the query.</li>
</ul></li>
</ul>

<pre><code class="language-javascript">...
&lt;template v-slot="{ currentRefinement, indices, refine }"&gt;
...</code></pre>

<ul>
<li>Create a search <code>&lt;input&gt;</code> to hold, change the query and value of the <code>currentRefinement</code>.</li>
</ul>

<pre><code class="language-javascript">...
&lt;input
    type="search"
    :value="currentRefinement"
    placeholder="Search for an article"
    @input="refine($event.currentTarget.value)"
/&gt;
...</code></pre>

<ul>
<li>Render the search results for each index. Each index has the following properties:

<ul>
<li><strong><code>indexName: string</code></strong>: the name of the index.</li>
<li><strong><code>indexId: string</code></strong>: the id of the index.</li>
<li><strong><code>hits: object[]</code></strong>: the resolved hits from the index matching the query.</li>
</ul></li>
</ul>

<pre><code class="language-javascript">...
&lt;template v-if="currentRefinement"&gt;
    &lt;ul v-for="index in indices" :key="index.indexId"&gt;
        &lt;li&gt;
            &lt;h3&gt;{{ index.indexName }}&lt;/h3&gt;
            
...</code></pre>

<ul>
<li>Then render the results — <code>hits</code>.</li>
</ul>

<pre><code class="language-javascript">...
&lt;ul&gt;
    &lt;li v-for="hit in index.hits" :key="hit.objectID"&gt;
      &lt;h1&gt;
        &lt;ais-highlight attribute="title" :hit="hit" /&gt;
      &lt;/h1&gt;
      &lt;h2&gt;
        &lt;ais-highlight attribute="description" :hit="hit" /&gt;
      &lt;/h2&gt;
      &lt;p&gt;
        &lt;ais-snippet attribute="bodyPlainText" :hit="hit" /&gt;
      &lt;/p&gt;
    &lt;/li&gt;
&lt;/ul&gt;

...</code></pre>

<p>Here’s what we’re using:</p>

<ul>
<li><strong><code>&lt;ais-highlight&gt;</code></strong><br />
Widget to highlight the portion of the result which directly matches the query of the field passed to the <code>attribute</code> prop.</li>
<li><strong><code>&lt;ais-snippet&gt;</code></strong><br />
Widget to display the relevant section of the snippeted attribute and highlight it. We defined the <code>attribute</code> in <code>attributesToSnippet</code> in <code>&lt;ais-configure&gt;</code>.</li>
</ul>

<p>Let’s run our dev server and see what our New search looks like.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/da9fad78-4dd8-41fa-8dd0-9352e609f249/search-component-with-vue-instantsearch-widgets.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="409"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/da9fad78-4dd8-41fa-8dd0-9352e609f249/search-component-with-vue-instantsearch-widgets.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/da9fad78-4dd8-41fa-8dd0-9352e609f249/search-component-with-vue-instantsearch-widgets.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/da9fad78-4dd8-41fa-8dd0-9352e609f249/search-component-with-vue-instantsearch-widgets.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/da9fad78-4dd8-41fa-8dd0-9352e609f249/search-component-with-vue-instantsearch-widgets.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/da9fad78-4dd8-41fa-8dd0-9352e609f249/search-component-with-vue-instantsearch-widgets.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/da9fad78-4dd8-41fa-8dd0-9352e609f249/search-component-with-vue-instantsearch-widgets.png"
			
			sizes="100vw"
			alt="Search component with vue instantsearch widgets "
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      New Search component. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/da9fad78-4dd8-41fa-8dd0-9352e609f249/search-component-with-vue-instantsearch-widgets.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="styling-our-search-component">Styling Our Search Component</h2>

<p>InstantSearch comes with some default styles that we included in our project using the <code>instantsearch.css</code> package. However, we might need to change or add some styles to our components to suit the site we’re building.</p>

<p>The CSS classes with many widgets can be overwritten using the <a href="https://www.algolia.com/doc/api-reference/widgets/highlight/vue/#widget-param-class-names"><code>class-names</code></a> prop. For example, we can change the highlighted style of <code>&lt;ais-highlight&gt;</code>.</p>

<pre><code class="language-javascript">&lt;!-- components/Search.vue --&gt;

...
&lt;h1&gt;
  &lt;ais-highlight
    :class-names="{
      'ais-Highlight-highlighted': 'customHighlighted',
    }"
    attribute="title"
    :hit="hit"
  /&gt;
&lt;/h1&gt;

...</code></pre>

<p>And in our CSS:</p>

<pre><code class="language-javascript">&lt;!-- components/Search.vue --&gt;

...

&lt;style&gt;
    .customHighlighted {
      @apply text-white bg-gray-600;
    }
&lt;/style&gt;
...</code></pre>

<p>We see that the class we defined has been applied to the highlight.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fd3e70d3-9939-4aba-8157-dfcf0f8ca4eb/screenshot-of-site-and-devtools-showing-custom-css-classes.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fd3e70d3-9939-4aba-8157-dfcf0f8ca4eb/screenshot-of-site-and-devtools-showing-custom-css-classes.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fd3e70d3-9939-4aba-8157-dfcf0f8ca4eb/screenshot-of-site-and-devtools-showing-custom-css-classes.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fd3e70d3-9939-4aba-8157-dfcf0f8ca4eb/screenshot-of-site-and-devtools-showing-custom-css-classes.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fd3e70d3-9939-4aba-8157-dfcf0f8ca4eb/screenshot-of-site-and-devtools-showing-custom-css-classes.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fd3e70d3-9939-4aba-8157-dfcf0f8ca4eb/screenshot-of-site-and-devtools-showing-custom-css-classes.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fd3e70d3-9939-4aba-8157-dfcf0f8ca4eb/screenshot-of-site-and-devtools-showing-custom-css-classes.png"
			
			sizes="100vw"
			alt="Screenshot of site and devtools showing custom css classes"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Custom classes for `<ais-highlight>` component. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/fd3e70d3-9939-4aba-8157-dfcf0f8ca4eb/screenshot-of-site-and-devtools-showing-custom-css-classes.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>So, I’ll go ahead and style it using tailwind till I feel it looks good.</p>

<pre><code class="language-javascript">&lt;!-- components/Search.vue --&gt;

&lt;template&gt;
  &lt;div class="search-cont relative inline-flex mt-6 bg-gray-100 border-2 rounded-lg focus-within:border-purple-600"&gt;
    &lt;ais-instant-search-ssr index-name="articles" :search-client="searchClient"&gt;
      &lt;ais-configure :attributesToSnippet="['bodyPlainText']" :hits-per-page.camel="5"&gt;
        &lt;ais-autocomplete class="wrapper relative"&gt;
          &lt;div slot-scope="{ currentRefinement, indices, refine }"&gt;
            &lt;input class="p-2 bg-white bg-opacity-0 outline-none" type="search" :value="currentRefinement" placeholder="Search for an article" @input="refine($event.currentTarget.value)" /&gt;
            &lt;div class="results-cont relative"&gt;
              &lt;div
                class=" absolute max-h-96 overflow-y-auto w-96 top-2 left-0 bg-white border-2 rounded-md shadow-lg" v-if="currentRefinement"&gt;
                &lt;ais-stats class="p-2" /&gt;
                &lt;ul v-for="index in indices" :key="index.indexId"&gt;
                  &lt;template v-if="index.hits.length &gt; 0"&gt;
                    &lt;li&gt;
                      &lt;h2 class="font-bold text-2xl p-2"&gt;
                        {{ index.indexName }}
                      &lt;/h2&gt;
                      &lt;ul&gt;
                        &lt;li
                          class="border-gray-300 border-t p-2 hover:bg-gray-100" v-for="hit in index.hits" :key="hit.objectID" &gt;
                          &lt;nuxt-link
                            :to="{
                              name: 'blog-slug',
                              params: { slug: hit.objectID },
                            }"
                          &gt;
                            &lt;h3 class="font-extrabold text-xl"&gt;
                              &lt;ais-highlight
                                :class-names="{
                                  'ais-Highlight-highlighted':
                                    'customHighlighted',
                                }"
                                attribute="title"
                                :hit="hit"
                              /&gt;
                            &lt;/h3&gt;
                            &lt;p class="font-bold"&gt;
                              &lt;ais-highlight
                                :class-names="{
                                  'ais-Highlight-highlighted':
                                    'customHighlighted',
                                }"
                                attribute="description"
                                :hit="hit"
                              /&gt;
                            &lt;/p&gt;
                            &lt;p class="text-gray-500"&gt;
                              &lt;ais-snippet
                                :class-names="{
                                  'ais-Snippet-highlighted':
                                    'customHighlighted',
                                }"
                                attribute="bodyPlainText"
                                :hit="hit"
                              /&gt;
                            &lt;/p&gt;
                          &lt;/nuxt-link&gt;
                        &lt;/li&gt;
                      &lt;/ul&gt;
                    &lt;/li&gt;
                  &lt;/template&gt;
                &lt;/ul&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/ais-autocomplete&gt;
      &lt;/ais-configure&gt;
    &lt;/ais-instant-search-ssr&gt;
  &lt;/div&gt;
&lt;/template&gt;

...

&lt;style&gt;
.customHighlighted {
  @apply text-purple-600 bg-purple-100 rounded p-1;
}
&lt;/style&gt;</code></pre>

<p>Alright, the styling is done and I’ve included a <code>&lt;nuxt-link&gt;</code> to route to the article on click.</p>

<pre><code class="language-javascript">&lt;nuxt-link :to="{ name: 'blog-slug', params: { slug: hit.objectID }}"&gt;</code></pre>

<p>We now have something like this:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/9320ce28-b049-4389-950b-e83fde8164a0/screenshot-of-search-component-with-styling.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="407"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/9320ce28-b049-4389-950b-e83fde8164a0/screenshot-of-search-component-with-styling.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/9320ce28-b049-4389-950b-e83fde8164a0/screenshot-of-search-component-with-styling.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/9320ce28-b049-4389-950b-e83fde8164a0/screenshot-of-search-component-with-styling.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/9320ce28-b049-4389-950b-e83fde8164a0/screenshot-of-search-component-with-styling.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/9320ce28-b049-4389-950b-e83fde8164a0/screenshot-of-search-component-with-styling.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/9320ce28-b049-4389-950b-e83fde8164a0/screenshot-of-search-component-with-styling.png"
			
			sizes="100vw"
			alt="Screenshot of search component with styling"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Styled Search component. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/9320ce28-b049-4389-950b-e83fde8164a0/screenshot-of-search-component-with-styling.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="configuring-instantsearch-for-server-side-rendering-ssr">Configuring InstantSearch For Server-Side Rendering (SSR)</h2>

<p>We now have our search component up and running but it only renders on the client-side and this means we have to wait for the search component to load even after the page loads. We can further improve the performance of our site by rendering it on the server-side.</p>

<p>According to <a href="https://www.algolia.com/doc/guides/building-search-ui/going-further/server-side-rendering/vue/#with-nuxt">Algolia</a>, the steps for implementing server-side rendering are:</p>

<p>On the server:</p>

<ul>
<li>Make a request to Algolia to get search results.</li>
<li>Render the Vue app with the results of the request.</li>
<li>Store the search results on the page.</li>
<li>Return the HTML page as a string.</li>
</ul>

<p>On the client:</p>

<ul>
<li>Read the search results from the page.</li>
<li>Render (or hydrate) the Vue app with search results.</li>
</ul>

<h3 id="using-mixins-serverprefetch-beforemount">Using Mixins, <code>serverPreFetch</code>, <code>beforeMount</code></h3>

<p>Following Algolia’s documentation on implementing SSR with Nuxt, we have to make the following changes:</p>

<pre><code class="language-javascript">&lt;!-- components/Search.vue --&gt;

...
&lt;script&gt;
// import 'vue-instantsearch';
import { createServerRootMixin } from 'vue-instantsearch'

import algoliaSearch from 'algoliasearch/lite'
import 'instantsearch.css/themes/satellite-min.css'

const searchClient = algoliaSearch(
  '34IIDW6KKR',
  '3f8d80be6c42bb030d27a7f108eb75f8'
)

export default {
  data() {
    return {
      searchClient,
    }
  },

  mixins: [
    createServerRootMixin({
      searchClient,
      indexName: 'articles',
    }),
  ],

  serverPrefetch() {
    return this.instantsearch.findResultsState(this).then((algoliaState) =&gt; {
      this.$ssrContext.nuxt.algoliaState = algoliaState
    })
  },

  beforeMount() {
    const results =
      (this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
      window.__NUXT__.algoliaState

    this.instantsearch.hydrate(results)

    // Remove the SSR state so it can’t be applied again by mistake
    delete this.$nuxt.context.nuxtState.algoliaState
    delete window.__NUXT__.algoliaState
  },
}
&lt;/script&gt;</code></pre>

<p>We’re simply doing the following:</p>

<ul>
<li><strong><code>createServerRootMixin</code></strong> to create a reusable search instance;</li>
<li><strong><code>findResultsState</code></strong> in <strong><code>serverPrefetch</code></strong> to perform a search query on the back end;</li>
<li><strong><code>hydrate</code></strong> method in <strong><code>beforeMount</code></strong>.</li>
</ul>

<p>Then in our <code>&lt;template&gt;</code>,</p>

<pre><code class="language-javascript">&lt;!-- components/Search.vue --&gt;

...
&lt;ais-instant-search-ssr index-name="articles" :search-client="searchClient"&gt;
    ...
&lt;/ais-instant-search-ssr&gt;
...</code></pre>

<p>Here, we to replace <code>ais-instant-search</code> with <code>ais-instant-search-ssr</code>.</p>

<h2 id="conclusion">Conclusion</h2>

<p>We’ve successfully built a Nuxt site with some content handled by Nuxt Content and integrated a simple Algolia search into our site. We’ve also managed to optimize it for SSR. I have a link to the source code of the project in this tutorial and a demo site deployed on Netlify, the links are down below.</p>

<p>We have tons of options available to customize and provide a rich search experience now that the basics are out of the way. <a href="https://www.algolia.com/doc/guides/building-search-ui/widgets/showcase/vue/">The Algolia widgets showcase</a> is a great way to explore those options and widgets. You’ll also find more information on the <a href="https://www.algolia.com/developers/code-exchange/dynamic-widgets-widget-1">widgets</a> used in this tutorial.</p>

<h3 id="github-source-code">GitHub Source Code</h3>

<ul>
<li>You can check out the source code <a href="https://github.com/miracleonyenma/algolia-nuxt">here</a>.</li>
<li>You can play with the demo on <a href="https://algolia-nuxtx.netlify.app/">https://algolia-nuxtx.netlify.app/</a>.</li>
</ul>

<h3 id="other-resources">Other Resources</h3>

<p>Here are some links that I think you will find useful:</p>

<ul>
<li><a href="https://nuxtjs.org/blog/creating-blog-with-nuxt-content">Create a Blog with Nuxt Content</a> by Debbie O’Brien</li>
<li><a href="https://content.nuxtjs.org/"><code>@nuxt/content</code> Module</a></li>
<li><a href="https://tailwindcss.com/docs">Tailwindcss docs</a></li>
<li><a href="https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/vue/">Vue InstantSearch</a></li>
</ul>

<div class="partners__lead-place"></div>

<h3 id="further-reading">Further Reading</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2024/12/introduction-css-scroll-driven-animations/">An Introduction To CSS Scroll-Driven Animations: Scroll And View Progress Timelines</a></li>
<li><a href="https://www.smashingmagazine.com/2024/06/uniting-web-native-apps-unknown-javascript-apis/">Uniting Web And Native Apps With 4 Unknown JavaScript APIs</a></li>
<li><a href="https://www.smashingmagazine.com/2023/05/big-tooling-upgrades-large-organizations/">How To Deal With Big Tooling Upgrades In Large Organizations</a></li>
<li><a href="https://www.smashingmagazine.com/2020/07/accessible-front-end-application-chakra-ui-nuxtjs/">How To Build An Accessible Front-End Application With Chakra UI And Nuxt.js</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(ks, vf, yk, il, mrn)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Marcus Herrmann</author><title>Three Insights I Gained While Researching Vue.js Accessibility</title><link>https://www.smashingmagazine.com/2021/07/three-insights-vuejs-accessibility/</link><pubDate>Fri, 16 Jul 2021 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2021/07/three-insights-vuejs-accessibility/</guid><description>Web app accessibility appears difficult because it seems that there is little information on the subject available online. But while researching for my eBook, I found that the situation is better than it seems. What follows are three insights about the accessible use of framework features, concrete Vue.js traits you can use for the inclusive cause, as well as community initiatives and vetted patterns (and where to find them).</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2021/07/three-insights-vuejs-accessibility/" />
              <title>Three Insights I Gained While Researching Vue.js Accessibility</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Three Insights I Gained While Researching Vue.js Accessibility</h1>
                  
                    
                    <address>Marcus Herrmann</address>
                  
                  <time datetime="2021-07-16T13:00:00&#43;00:00" class="op-published">2021-07-16T13:00:00+00:00</time>
                  <time datetime="2021-07-16T13:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>JavaScript frameworks like React, Angular and Vue have a very bad reputation when it comes to web accessibility. But is this due to inherent technical limitations or insurmountable problems of those tools? I think not. During the research phase of my book, “<a href="https://accessible-vue.com/">Accessible Vue</a>,” I gained three insights regarding web app accessibility in general and the framework in particular. Considering these, perhaps it’s worth taking another perspective around accessible Vue apps.</p>

<h2 id="insight-1-javascript-framework-features-for-accessibility-are-underused">Insight 1: JavaScript Framework Features For Accessibility Are Underused</h2>

<p>Component-based design, enabled and enforced by modern JavaScript frameworks, does not only provide great developer experiences and project ergonomics when used in a smart way, but it can also offer advantages for accessibility. The first is <strong>the factor of reusability</strong>, i.e. when your component gets used in several places within your app (perhaps in different forms or shapes) and it only has to be made accessible only once. In this case, an increased developer experience actually helps the user and “baking accessibility into components” (<a href="https://hiddedevries.nl/en/blog/2019-05-24-baking-accessibility-into-components-how-frameworks-help">as Hidde de Vries puts it)</a> creates a win-win scenario for everyone.</p>

<p>The second aspect that comes with component based-designs are <code>props</code> &mdash; namely in the form that one component can inherit or get context from its parent environment. This forwarding of “environment data” can serve accessibility as well.</p>

<p>Take headlines, for example. A solid and comprehensible headline structure is not only good for SEO but especially for people using screen readers. When they encounter a sound document outline, constructed with headlines that structure a web page or app, screen reader users gain a quick overview of the web page they are on. Just like visually-abled users don’t read every word on a page but scan for interesting things, <strong>blind screen reader users don’t make their software read each and every word</strong>. Instead, they are checking a document for content and functionality they are interested in. Headlines, for that matter, are keeping pieces of content together and are at the same time providing a structural frame of a document (<a href="https://en.wikipedia.org/wiki/Timber_framing">think timber frame houses</a>).</p>

<p>What makes headlines providing a structure is not only their mere existence. It is also their nesting that creates an image inside a user’s mind. For that, a web developer’s headline toolbox contains six levels (<code>&lt;h1&gt;</code> to <code>&lt;h6&gt;</code>). By applying these levels, both editors and developers can create an outline of content and a reliable functionality that users can expect in the document.</p>

<p>For example, let’s take the (abridged) headline tree from the <a href="https://www.gov.uk/">GOV.UK website</a>:</p>

<pre><code class="language-markup">1 &mdash; Welcome to GOV.UK
  2 &mdash; Popular on GOV.UK
  2 &mdash; Services and information
    3 &mdash; Benefits
    3 &mdash; Births, deaths, marriages and care
    3 &mdash; Business and self-employment
    // …etc
  2 &mdash; Departments and policy
    3 &mdash; Coronavirus (COVID 19)
    3 &mdash; Travel abroad: step by step
…etc
</code></pre>

<p>Even without visiting the actual page and without actually perceiving it visually, this headline tree created a table of contents helping you understand what sections can be expected on the front page. The creators used headline elements <a href="https://hiddedevries.nl/en/blog/2020-09-05-when-there-is-no-content-between-headings">to herald data following it</a> and didn’t skip headline levels.</p>

<p>So far, so familiar (at least in correlation with search engines, I guess). However, because a component can be used in different places of your app, hardwired headline levels inside them can sometimes create a suboptimal headline tree overall. <strong>Relations between headlines</strong> possibly aren’t conveyed as clear as in the example above (“Business and self-employment” does not stand on its own but is related to “Services and information”).</p>

<p>For example, imagine a listing of a shop’s newest products that can be placed both in the main content and a sidebar &mdash; it’s quite possible that both sections live in different contexts. A headline such as <code>&lt;h1&gt;Our latest arrivals&lt;/h1&gt;</code> would make sense above the product list in the main content &mdash; given it is the central content of the whole document or view.</p>

<p>The same component sporting the same <code>&lt;h1&gt;</code>  but placed in a sidebar of another document, however, would suggest the most important content lives in the sidebar and competes with the <code>&lt;h1&gt;</code> in the main content. While what I described above is a peculiarity of component-based design in general this gives us a perfect opportunity to put both aspects together &mdash; the need for a sound headline tree and our knowledge about props:</p>

<h3 id="context-via-props">Context Via <code>props</code></h3>

<p>Let’s progress from theoretical considerations into hands-on code. In the following code block, you see a component listing the newest problems in an online shop. It is extremely simplyified but the emphasis is on line 3, the hardcoded <code>&lt;h1&gt;</code>:</p>

<pre><code class="language-html">&lt;template&gt;
    &lt;div&gt;
        &lt;h1&gt;Our latest arrivals&lt;/h1&gt;
        &lt;ol&gt;
            &lt;li&gt;Product A&lt;/li&gt;
            &lt;li&gt;Product B&lt;/li&gt;
            &lt;!-- etc --&gt;
        &lt;/ol&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>

<p>To use this component in different places of the app without compromising the document’s headline tree, we want to make the headline level dynamic. To achieve this, we replace the <code>&lt;h1&gt;</code> with Vue’s <a href="https://vuejs.org/v2/guide/components.html#Dynamic-Components">dynamic component name</a> helper called, well, <code>component</code>:</p>

<pre><code class="language-html">&lt;component :is="headlineElement"&gt;Our latest arrivals&lt;/component&gt;
</code></pre>

<p>In the script part of our component, we now have to add two things:</p>

<ul>
<li>A component prop that receives the exact headline level as a string, <code>headlineLevel</code>;</li>
<li>A computed property (<code>headlineElement</code> from the code example above) that builds a proper HTML element out of the string <code>h</code> and the value of <code>headlineLevel</code>.</li>
</ul>

<p>So our simplified script block looks like this:</p>

<pre><code class="language-html">&lt;script&gt;
export default {
    props: {
      headlineLevel: {
        type: String
    },
    computed: {
        headlineElement() {
          return "h" + this.headlineLevel;
        }
    }
}
&lt;/script&gt;
</code></pre>

<p>And that’s all!</p>

<p>Of course, adding checks and sensible defaults on the prop level is necessary &mdash; for example, we have to make sure that <code>headlineLevel</code> can only be a number between 1 and 6. Both Vue’s native <a href="https://vuejs.org/v2/guide/components-props.html#Prop-Validation">Prop Validation</a>, as well as TypeScript, are tools at your disposal to do just that, but I wanted to keep it out of this example.</p>

<p>If you happen to be interested in learning how to accomplish the exact same concept using React, <a href="https://medium.com/@Heydon/managing-heading-levels-in-design-systems-18be9a746fa3">friend of the <del>show</del> magazine Heydon Pickering wrote about the topic back in 2018</a> and supplied React/JSX sample code. Tenon UI’s Heading Components, also written for React, take this concept even further and aim to automate headline level creation by using so-called “LevelBoundaries” and a generic <code>&lt;Heading&gt;</code> element. <a href="https://www.tenon-ui.info/headings/">Check it out</a>!</p>

<h2 id="insight-2-there-are-established-strategies-to-tackle-web-app-accessibility-problems">Insight 2: There Are Established Strategies To Tackle Web App Accessibility Problems</h2>

<p>While web app accessibility may look daunting the first time you encounter the topic, there’s no need to despair: vested accessibility patterns to tackle typical web app characteristics do exist. In the following Insight, I will introduce you to strategies for supplying <strong>accessible notifications</strong>, including an easy implementation in Vue.js (<a href="#strategy-1-announcing-dynamic-updates-with-live-regions">Strategy 1</a>), then point you towards recommended patterns and their Vue counterparts (<a href="#strategy-2-using-undisputed-wai-aria-authoring-practices">Strategy 2</a>). Lastly, I recommend taking a look at both Vue’s emerging (<a href="#strategy-3-view-and-help-vue-s-accessibility-initiatives-grow">Strategy 3</a>) and React’s established accessibility community (<a href="#strategy-4-learn-from-react-accessibility-leads">Strategy 4</a>).</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="/printed-books/touch-design-for-mobile-interfaces/">Touch Design for Mobile Interfaces</a></strong>, Steven Hoober’s brand-new guide on <strong>designing for mobile</strong> with proven, universal, human-centric guidelines. <strong>400 pages</strong>, jam-packed with in-depth user research and <strong>best practices</strong>.</p>
<a data-instant href="https://www.smashingmagazine.com/printed-books/touch-design-for-mobile-interfaces/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://www.smashingmagazine.com/printed-books/touch-design-for-mobile-interfaces/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/14bcab88-b622-47f6-a51d-76b0aa003597/touch-design-book-shop-opt.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b14658fc-bb2d-41a6-8d1a-70eaaf1b8ec8/touch-design-book-shop-opt.png"
    alt="Feature Panel"
    width="480"
    height="697"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h3 id="strategy-1-announcing-dynamic-updates-with-live-regions">Strategy 1: Announcing Dynamic Updates With Live Regions</h3>

<p>While accessibility is more than making things screen reader compatible, improving the screen reader experience plays a big part of web app accessibility. This is rooted in the general working principle of this form of assistive technology: screen reader software transforms content on the screen into <strong>either audio or braille output</strong>, thus enabling blind people to interact with the web and technology in general.</p>

<p>Like keyboard focus, a screen reader’s output point, the so-called <strong>virtual cursor</strong>, can only be at one place at once. At the same time, one core aspect of web apps is a dynamic change in parts of the document without page reload. But what happens, for example, when the update in the DOM is actually <em>above</em> the virtual cursor’s position in the document? Users likely would not notice the change because do not tend to traverse the document in reverse &mdash; unless they are somehow informed of the dynamic update.</p>

<p>In the following short video, I demonstrate what happens (or rather, what <em>not</em> happens) if an interaction causes a <strong>dynamic DOM change</strong> nowhere near the virtual cursor &mdash; the screen reader just stays silent:</p>


<figure class="video-embed-container">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="a7ZnV_D-hDY"
      
			
		></lite-youtube>
	</div>
	
</figure>

<p>But by using <strong>ARIA Live Regions</strong>, web developers can trigger accessible announcements, namely screen reader output independent of the virtual cursor’s position. The way live regions work is that a screen reader is instructed to <strong>observe certain HTML elements</strong>’ <code>textContent</code>. When it changes due to scripting, the screen reader picks up the update, and the new text will be read out.</p>

<p>As an example, imagine a list of products in an online shop. Products are listed in a table and users can add every product to their shopping cart without a page reload by the click of a button. The expected asynchronous update of the DOM, while perceivable for visual users, is a perfect job for live regions.</p>

<p>Let’s write a piece of simplified dream code for this situation. Here’s the HTML:</p>

<pre><code class="language-html">&lt;button id="addToCartOne"&gt;Add to cart&lt;/button&gt;

&lt;div id="info" aria-live="polite"&gt;
&lt;!-- I’m the live region. For the sake of this example, I'll start empty. 
     But screen readers detect any text changes within me! --&gt;
&lt;/div&gt;
</code></pre>

<p>Now, both DOM updates and live regions announcements are only possible with JavaScript. So let’s look at the equally simplified script part of our “Add to Cart” button click handler:</p>

<pre><code class="language-html">&lt;script&gt;
const buttonAddProductOneToCart = document.getElementById('addToCartOne');
const liveRegion = document.getElementById('info');

buttonAddProductOneToCart.addEventListener('click', () =&gt; {
        // The actual adding logic magic 🪄

        // Triggering the live region:
        liveRegion.textContent = "Product One has been added to your cart";
});
&lt;/script&gt;
</code></pre>

<p>You can see in the code above that when the actual adding happens (the actual implementation depends on your data source and tech stack, of course), an <strong>accessible announcement</strong> gets triggered. The once empty <code>&lt;div&gt;</code> with the ID of <code>info</code> changes its text content to “Product One has been added to your cart”. Because screen readers observe the region for changes like this, a screen reader output regardless of the virtual cursor position is prompted. And because the live region is set to <code>polite</code>, the announcement <em>waits</em> in case there is a current output.</p>

<p>If you really want to convey an important announcement that doesn’t respect the current screen reader message but interrupts it, the <code>aria-live</code> attribute can also be set to <code>assertive</code>. <strong>Live regions</strong> in themselves are powerful tools that should be used with caution, which is even more valid for this more &ldquo;aggressive&rdquo; kind. So please limit their use to urgent error messages the user <em>must</em> know about, for example, “Autosave failed, please save manually”.</p>

<p>Let’s visit our example from above again, this time with implemented live regions: Screen reader users are now informed that their button interaction has worked and that the particular item has been added to (or removed from) their shopping cart:</p>


<figure class="video-embed-container">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="avYhLLo8f7k"
      
			
		></lite-youtube>
	</div>
	
</figure>

<p>If you want to use live regions in Vue.js applications, you can, of course, recreate the code examples above. However, an easier way would be to use the library <a href="https://github.com/vue-a11y/vue-announcer">vue-announcer</a>. After installing it with <code>npm install -S @vue-a11y/announcer</code> (or <code>npm install -S @vue-a11y/announcer@next</code> for the Vue 3 version) and <a href="https://vue-a11y.com/posts/vue-announcer/#meet-vue-announcer">registering it a Vue plugin</a>, there are only two steps necessary:</p>

<ol>
<li>The placement of <code>&lt;VueAnnouncer /&gt;</code> in your <code>App.vue</code>’s template. This renders an empty live region (like the one from above that had the ID of <code>info</code>).<br />
<strong>Note</strong>: <em>It is recommended only to use one live region instance, and it makes sense to place it in a central place, so that many components can refer to it.</em><br /></li>
</ol>

<pre><code class="language-markup">&lt;template&gt;
  &lt;div&gt;
    &lt;VueAnnouncer /&gt;
    &lt;!-- ... --&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>

<ol>
<li>The triggering of the live region, for example from within a method or lifecycle hook. The easiest way to accomplish this is using the <code>.set</code>  method or <code>this.$announcer</code>. The method’s first parameter is the text the live regions is updated with (equivalent to the text that the screen reader will output). As a second parameter, you can explicitly supply <code>polite</code> and <code>assertive</code> as a setting). But, as you’ll notice it is optional &mdash; in case the parameter gets omitted the announcement will be a polite one:<br /></li>
</ol>

<pre><code class="language-js">methods: {
  addProductToCart(product) {
    // Actual adding logic here

    this.$announcer.set(&#96;${product.title} has been added to your cart.&#96;);
  }
}
</code></pre>

<p>This was just a small peek into the amazing world of ARIA live regions. As a matter of fact, more options than polite and assertive are available (such as <code>log</code>, <code>timer</code> and even <code>marquee</code>) but with varying support in screen readers.</p>

<p>If you want to dive deeper into the topic, here are three recommended resources:</p>

<ul>
<li>“<a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions">ARIA Live Regions</a>,” MDN Web Docs</li>
<li>“<a href="https://www.youtube.com/watch?v=W5YAaLLBKhQ">The Many Lives Of A Notification</a>,” Sarah Higley (video)</li>
<li><a href="https://chrome.google.com/webstore/detail/nerderegion/lkcampbojgmgobcfinlkgkodlnlpjieb">NerdeRegion</a>, a Chrome extension that lets you roughly emulate live region output in your dev tools without the need to fire up a screen reader. However, this should not replace conscientious testing in real screen readers!</li>
</ul>

<h3 id="strategy-2-using-undisputed-wai-aria-authoring-practices">Strategy 2: Using Undisputed WAI-ARIA Authoring Practices</h3>

<p>The moment you encounter <a href="https://www.w3.org/TR/wai-aria-practices-1.1/">WAI-ARIA’s authoring practices</a>, you&rsquo;ll likely feel a great relief. It seems that the web’s standard body, the W3 Consortium, offers some kind of pattern library that you just have to use (or convert to your framework of choice), and boom, all your web app accessibility challenges are solved.</p>

<p>The reality, however, is not so simple. While it is true that the W3C offers a plethora of typical web app patterns like combo boxes, sliders, menus and treeviews, not <em>all</em> authoring practices are in a state that they are recommended for production. The actual idea behind authoring practices was to demonstrate the <strong>&ldquo;pure&rdquo; use of ARIA states</strong>, roles and widget patterns.</p>

<p>But in order to be a truly vetted pattern, its authors have to make sure that every practice has solid support among assistive technologies and also works seamlessly for touch devices. Alas, that’s the place where some patterns listed in the Authoring Practices fall short. Web development is in a state of constant flux, and likely web app evolution even more so. A good place to stay updated with the state of single authoring practices is W3C’s <a href="https://github.com/w3c/aria-practices/issues">authoring-practices repo on GitHub</a>. In the Issues section, web accessibility experts exchange their current ideas, experiences, and testing research for every pattern.</p>

<p>All that being said does not mean that the practices have no value at all for your web app accessibility ventures, though. While there are widgets that are mere proofs of concept there are solid patterns. In the following, I want to highlight three undisputed Authoring Practices and their counterparts in built-in Vue.js:</p>

<ul>
<li><p><a href="https://www.w3.org/TR/wai-aria-practices-1.1/#disclosure">Disclosure Widgets</a> are simple and straightforward concepts that can be used in a variety of ways: as a basis for your accessible accordion, as part of a robust dropdown navigation menu, or to show and hide additional information, like extended image descriptions.<br /><br />The great thing about the pattern is that it only consists of two elements: A trigger (1) that toggles the visibility of a container (2). In HTML terms, trigger and container must directly follow each other in the DOM. To learn more about the concept and the implementation in Vue, read my <a href="https://marcus.io/blog/disclosure-widget-vuejs">blog article</a> about Disclosure Widgets in Vue, or <a href="https://codesandbox.io/s/accvue-disclosure-vue2-cv3ss">check out the corresponding demo on CodeSandBox</a>.</p></li>

<li><p><a href="https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html">Modal dialogs</a> are also considered an established pattern. What makes a dialog &ldquo;modal&rdquo; is its property to render the parts of the interface that are not the modal’s content inactive when it’s open.<br /><br />Furthermore, developers must ensure that the keyboard focus is sent into the modal upon activation, can’t leave an open modal and is being sent back to the triggering control after deactivation. <a href="https://github.com/KittyGiraudel/a11y-dialog">Kitty Giraudel’s A11y Dialog component</a> takes care of all the things I just described. For developers using Vue.js, there is a plugin called <a href="https://github.com/morkro/vue-a11y-dialog">vue-a11y-dialog</a> available.</p></li>

<li><p><a href="https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel">Tab Components</a> are a common dynamic pattern that works with the metaphor of physical folder tabs and thus, helps authors to pack larger amounts of content into &ldquo;tab panels&rdquo;. The authoring practice comes in two variants related to panel activation (automatic or manual).<br /><br />What is even more important, tab components enjoy good support in assistive technology and can be therefore considered a recommended pattern (<a href="https://bbc.github.io/gel/components/tabs/#selecting-a-tab">as long as you test which activation mode works best for your users</a>). Architecturally speaking, there are multiple ways to build the tab component with the help of Vue.js: <a href="https://codesandbox.io/s/vue-tabs-pt5lm">In this CodeSandBox</a>, I decided to go for a slot-based approach and automatic activation.</p></li>
</ul>

<div class="partners__lead-place"></div>

<h3 id="strategy-3-view-and-help-vue-s-accessibility-initiatives-grow">Strategy 3: View And Help Vue’s Accessibility Initiatives Grow</h3>

<p>While there is still a way to go, it is true to state that the topic of accessibility in Vue.js is finally on the rise. A milestone for the topic was the addition of an <a href="https://v3.vuejs.org/guide/a11y-basics.html">&ldquo;Accessibility&rdquo; section in Vue’s official docs</a>, which happened related to the release of Vue 3.</p>

<p>But even apart from official resources, the following people from the Vue community are worth following because they provide either education material, accessible components, or both:</p>

<ul>
<li><strong>Maria Lombardo</strong> has the status of &ldquo;Vue community partner&rdquo;, penned the accessibility documentation linked above, is giving a11y-related workshops at Vue conferences, and has a (paid) Web Accessibility Fundamentals <a href="https://vueschool.io/courses/web-accessibility-fundamentals">course at vueschool.io</a>.</li>
<li>An article about accessibility in Vue.js would not be complete without a mention of <strong>Alan Ktquez,</strong> project lead of <a href="https://vue-a11y.com">vue-a11y.com</a>. He and his community initiative create and maintain plugins like the aforementioned <a href="https://github.com/vue-a11y/vue-announcer">vue-announcer</a>, <a href="https://github.com/vue-a11y/vue-skip-to">vue-skipto</a> for creating skip links, <a href="https://github.com/vue-a11y/vue-axe">vue-axe</a> as a Vue wrapper around Deque’s <code>axe-core</code> testing engine, and especially <a href="https://github.com/vue-a11y/awesome-a11y-vue">awesome-vue-a11y</a>, an ever-growing link list of accessibility resources in the Vueniverse.</li>
<li>Fellow Berliner <strong>Oscar Braunert</strong> has a special focus on inclusive inputs and shows how to implement them in Vue.js, for example in the form of <a href="https://talks.ovl.design/OY4mvA/accessibility-inputs-vue">talks</a> and <a href="https://www.ovl.design/text/inclusive-inputs/">articles</a>. With the <a href="https://ui.tournant.dev/">tournant</a> UI library, Oscar and I are aiming to supply accessible components both based on (undisputed) WAI Authoring Practices (see Strategy 2) and Heydon Pickering’s <a href="https://www.smashingmagazine.com/printed-books/inclusive-components/">Inclusive Components</a>.</li>
<li><strong>Moritz Kröger</strong> built a Vue wrapper for <a href="https://github.com/KittyGiraudel/a11y-dialog">Kitty Giraudel’s a11y-dialog</a> dubbed <a href="https://github.com/morkro/vue-a11y-dialog/">vue-a11y-dialog</a>, which provides everything a developer needs in term of semantics and focus management (see above).</li>
</ul>

<h3 id="strategy-4-learn-from-react-accessibility-leads">Strategy 4: Learn From React Accessibility Leads</h3>

<p>If you compare it with the top dog React.js, Vue.js is not a niche product, but you must admit that it has (not yet?) reached its popularity. However, this does not have to be a disadvantage when it comes to accessibility. React &mdash; and Angular before it &mdash; are in a sense pioneering in accessibility by their proliferation alone.</p>

<p>The more popular frameworks become, the higher the likelihood of good work in terms of inclusivity. Be it due to community initiatives on the subject or simply government authorities with web accessibility obligations doing a &ldquo;buy-in&rdquo;. If they also share their findings and accessible code via open-source, it’s a win-win-win situation. Not only for the framework itself and its community but also for &ldquo;competitors&rdquo;.</p>

<p>This actually has happened in the case of React (and the government project that I talked about so abstractly is the <a href="https://designsystem.gov.au/">Australian Government Design System</a>). Teams and developers caring for accessibility and working with React can check out projects like these and use the provided components and best practices.</p>

<p>Teams and developers caring for accessibility but using Vue.js, Angular, Svelte etc. can look into the React code and learn from it. Although there may be natural differences in the syntax of each framework, they have <strong>many basic concepts in common</strong>. Other React libraries that are considered accessible and are available as a basis for learning:</p>

<ul>
<li><a href="https://pattern-library.dequelabs.com/">Cauldron</a> from Deque</li>
<li><a href="https://www.tenon-ui.info/">Tenon UI</a> from Tenon</li>
<li><a href="https://baseweb.design/components/">BaseWeb Components</a> from Uber</li>
</ul>

<p>For improving Vue.js accessibility, it’s also worth following accessibility people from the React world:</p>

<ul>
<li>Marcy Sutton is a freelance web accessibility expert who worked in the past for Deque and improved accessibility and related documentation at Gatsby.js, which is a static site generator based on React. She is very hands-on, conducts research and conveys important topics regarding web app accessibility in great presentations, blog posts, and workshops. You can find Marcy Sutton on Twitter at <a href="https://twitter.com/marcysutton">@marcysutton</a>, web app-related courses on <a href="https://Egghead.io">Egghead.io</a> and <a href="https://testingaccessibility.com/">TestingAccessibility.com</a> or an overview of all of her projects by visiting her <a href="https://marcysutton.com/">website</a>.</li>
<li>Lindsey Kopacz is a web developer specializing in inaccessibility. She cares for inclusive experiences on the web, about overcoming ableism and educating her fellow web developers about the importance of accessibility. Apart from writing on her blog <a href="https://www.a11ywithlindsey.com/">a11ywithlindsey.com</a>, she also has <a href="https://egghead.io/q/resources-by-lindsey-kopacz">courses on Egghead.io</a> and lately published her ebook “<a href="https://www.a11ywithlindsey.com/gift/">The Bootcampers Guide to Web Accessibility</a>”. On Twitter, she is <a href="https://twitter.com/littlekope">@littlekope</a>.</li>
<li>Ryan Florence and Michael Jackson created <a href="https://reach.tech/">Reach UI</a>, a collection of components and tools that aims “to become the accessible foundation of your React-based design system.” Besides the fact that they have created some accessible standard components, it is especially noteworthy that their &ldquo;Reach UI Router&rdquo; (along with its accessibility features) will merge with the &ldquo;official&rdquo; React Router in the future.<br /><br />Although React does not do “first-class plugins” like Vue.js, this is excellent news because they created their router with inbuilt focus management. A feature and accessibility enhancement that will soon benefit everyone using React Router and their users.</li>
</ul>

<div class="partners__lead-place"></div>

<h2 id="insight-3-vue-s-refs-are-great-for-focus-management">Insight 3: Vue’s $refs Are Great For Focus Management</h2>

<h3 id="focus-management">Focus Management?</h3>

<p>You encountered a way of sending accessible announcements by the use of ARIA Live Regions in the last Insight. Another way to deal with the problems a highly dynamic document presents to screen-reader and keyboard users is to <strong>manage focus programmatically</strong>. Before I start explaining focus management any further, please be aware: Usually, it is bad to change focus via scripting, and you should refrain from doing so.</p>

<p>Users don’t want their focus to be messed with. If a focus change does happen and is entirely unexpected, such an intervention is either a nuisance or even becomes a real barrier. On the other hand, changing focus programmatically is sometimes the only sensible option in JavaScript-based widgets or apps to help users who <strong>rely on keyboard</strong> usage. However, in this case, the focus change has to be predictable. A good way of ensuring this predictability is to ensure that a focus change only happens after an interaction, such as a button or link click.</p>

<p>What are circumstances in which focus management is can improve an app’s accessibility?</p>

<ol>
<li>Focus management is needed when the content that is being affected by an interaction (e.g. <code>&lt;div&gt;</code>) does not follow the trigger (e.g. <code>&lt;button&gt;</code>) directly in the document. For example, the <a href="https://adrianroselli.com/2020/05/disclosure-widgets.html#ARIA">disclosure widget concept</a> assumes the container the button toggles is directly below the button in the DOM tree.<br /><br />This document structure, this <strong>proximity of trigger</strong> and reacting container, cannot be ensured in every widget, so the focus has to be managed actively. When a modal dialog opens after a button activation, it can’t be ensured that its HTML nodes directly follow the triggering button in the DOM. Thus, the focus has actively sent into the modal, ensuring that keyboard-only and screen reader users can use the particular widget.</li>
<li>When parts of the document have changed without a page reload or parts of the DOM have been updated (again, after an interaction such as a button click), it is appropriate to send focus to the added or changed content.<br /><br />An example of this is the <strong>navigation between routes</strong> (&ldquo;pages&rdquo;) in Single Page Apps: Since they don’t reload the HTML document (like static websites do), a keyboard-only or screen reader user is not sent to the top of the &ldquo;new page&rdquo;. Because what is happening is not a &ldquo;proper&rdquo; page load &mdash; but a modification of a certain part of the same page.</li>
</ol>

<p>You can see examples of bad and good focus management in the following demos, provided by <a href="https://www.smashingmagazine.com/author/manuelmatuzovic/">Manuel Matuzović</a>. Although the underlying framework (React) and the underlying UI pattern (modal dialog) differs, the problem remains the same:</p>

<p>An example for <strong>lack of focus management</strong>:</p>


<figure class="video-embed-container">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="eGyEQT8EDLs"
      
			videotitle="Keyboard focus stays in the inactive parts of the UI, “behind” the opened modal"
		></lite-youtube>
	</div>
	
		<figcaption>Keyboard focus stays in the inactive parts of the UI, “behind” the opened modal</figcaption>
	
</figure>

<p>An example for <strong>good focus management</strong>:</p>


<figure class="video-embed-container">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="9Z3imL-fqdU"
      
			videotitle="Focus is sent into the modal dialog upon opening"
		></lite-youtube>
	</div>
	
		<figcaption>Focus is sent into the modal dialog upon opening</figcaption>
	
</figure>

<p>That leaves responsible developers with the task of sending the keyboard focus to particular elements for your template. Luckily, JavaScript frameworks have the concept of DOM node references, or &ldquo;refs&rdquo;. In Vue.js, it is sufficient to add the <code>ref</code> attribute in an HTML node. Subsequently, a reference to this node is available in the <code>$this.refs</code> object. Finally, focusing an element programmatically is as easy as calling JavaScript’s native <code>.focus()</code> method on it.</p>

<p>For the next example, let’s assume we have a button somewhere in our component and apply a <code>ref</code> named <code>triggerButton</code> to it. We want to set focus to it once the user hits the <kbd>ESC</kbd> key. Our code for this would look as follows:</p>

<pre><code class="language-html">&lt;template&gt;
    &lt;div @keydown.esc="focusTriggerBtn"&gt;
        &lt;button ref="triggerButton"&gt;Trigger&lt;/button&gt;
    &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
export default {
//...
methods: {
    focusTriggerBtn() {
        this.$refs.triggerButton.focus();
    }
}
//...
}
&lt;/script&gt;
</code></pre>

<h2 id="modal-off-canvas-navigation">Modal Off-Canvas Navigation</h2>

<p>Another use of both refs and focus management would be the accessible implementation of an off-canvas navigation.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e15cfbad-5787-4c55-82a8-7bd8aa687007/off-canvas-navigation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e15cfbad-5787-4c55-82a8-7bd8aa687007/off-canvas-navigation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e15cfbad-5787-4c55-82a8-7bd8aa687007/off-canvas-navigation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e15cfbad-5787-4c55-82a8-7bd8aa687007/off-canvas-navigation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e15cfbad-5787-4c55-82a8-7bd8aa687007/off-canvas-navigation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e15cfbad-5787-4c55-82a8-7bd8aa687007/off-canvas-navigation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e15cfbad-5787-4c55-82a8-7bd8aa687007/off-canvas-navigation.png"
			
			sizes="100vw"
			alt="Wireframe of a slide in navigation drawer, positioned on the left. Two arrows are showing: When it slides in it pushes content to the right, and vice versa."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/e15cfbad-5787-4c55-82a8-7bd8aa687007/off-canvas-navigation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>In this case, you need to establish at least two refs: One for the trigger button that opens the navigation (let’s call it <code>navTrigger</code>), and one for the element that gains focus as soon as the navigation is visible (<code>navContainer</code> in this example, an element which needs <code>tabindex=&quot;-1&quot;</code> to be programmatically focusable). So that, when the trigger button is clicked, the focus will be sent into the navigation itself. And vice versa: As soon as the navigation closes, the focus must return to the trigger.</p>

<p>After having read the paragraphs above, I hope one thing becomes clear for you, dear reader: Once you understand the importance of focus management, you realize that all the necessary tools are at your fingertips &mdash; namely Vue’s <code>this.$refs</code> and JavaScript’s native <code>.focus()</code></p>

<h2 id="conclusion">Conclusion</h2>

<p>By highlighting some of my core findings regarding web app accessibility, I hope that I have been able to help reduce any diffuse fear of this topic that may have existed, and you now feel more confident to build accessible apps with the help of Vue.js (if you want to dive deeper into the topic, check out if my little ebook <a href="https://accessible-vue.com/">“Accessible Vue”</a> can help you along the journey).</p>

<p>More and more websites are becoming more and more app-like, and it would be sad if these amazing digital products were to remain so barrier-laden only because web developers don’t know exactly where to start with the topic. It’s a <strong>genuinely enabling moment</strong> once you realize that a vast majority of web app accessibility is actually “good old” web accessibility, and for the rest of it, cowpaths are already paved.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(vf, il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Uma Victor</author><title>Tools And Practices To Speed Up The Vue.js Development Process</title><link>https://www.smashingmagazine.com/2021/07/tools-practices-speed-up-vuejs-development-process/</link><pubDate>Thu, 08 Jul 2021 11:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2021/07/tools-practices-speed-up-vuejs-development-process/</guid><description>Even though Vue.js claims to have an approachable minimalist framework that can be incrementally adaptable, it can be a little bit overwhelming when starting as a Vue.js newbie. In this tutorial, Uma Victor will take a look at some tips and tools to help you become a better Vue developer. You will start with some helpful insights on organizing your projects for scale and other great points to note and then round it up with Tools and extensions that make writing Vuejs so much easier.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2021/07/tools-practices-speed-up-vuejs-development-process/" />
              <title>Tools And Practices To Speed Up The Vue.js Development Process</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Tools And Practices To Speed Up The Vue.js Development Process</h1>
                  
                    
                    <address>Uma Victor</address>
                  
                  <time datetime="2021-07-08T11:00:00&#43;00:00" class="op-published">2021-07-08T11:00:00+00:00</time>
                  <time datetime="2021-07-08T11:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>Throughout this tutorial, we will be looking at practices that should be adopted, things that should be avoided, and have a closer look at some helpful tools to make writing Vue.js easier. I’ll be focusing mostly on <a href="https://vuejs.org/v2/guide/">Vue 2</a> as most folks and organizations still use the older version. No reason to worry though, as most things mentioned here still apply to <a href="https://v3.vuejs.org/guide/introduction.html">Vue 3</a> since it’s just a supercharged and faster version. Still, if you already know Vue 2 and just want to learn about what’s new in Vue 3, then you can check out the <a href="https://v3.vuejs.org/guide/migration/introduction.html">migration guide</a> to learn more.</p>

<p><strong>Note:</strong> <em>This article is aimed at both beginners and seasoned developers who want to better their Vue.js skills. Basic knowledge of JavaScript and Vue.js will be of great benefit as you work your way throughout this tutorial.</em></p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="/printed-books/typescript-in-50-lessons/">“TypeScript in 50 Lessons”</a></strong>, our shiny new guide to TypeScript. With detailed <strong>code walkthroughs</strong>, hands-on examples and common gotchas. For developers who know enough <strong>JavaScript</strong> to be dangerous.</p>
<a data-instant href="/printed-books/typescript-in-50-lessons/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="/printed-books/typescript-in-50-lessons/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2732dfe9-e1ee-41c3-871a-6252aeda741c/typescript-panel.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c2f2c6d6-4e85-449a-99f5-58bd053bc846/typescript-shop-cover-opt.png"
    alt="Feature Panel"
    width="481"
    height="698"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="module-based-vs-file-based-project-structuring">Module-Based Vs File-Based Project Structuring</h2>

<p>Let’s start by looking at how to structure files by modules, how file-based structuring might not be a good idea when it comes to building projects of scale, and how to structure modules to fit in with business requirements.</p>

<p>As we are newly creating a project with Vue.js CLI, we are getting the default file structure that has been mapped out by the Vue.js team. Using the proposed file structure is not a bad way of structuring your project per se, but as your project grows, you will need a better structure as your code becomes clustered and harder to navigate and access files.</p>

<p>This is where the <strong>module-based method of structuring your project</strong> comes into play.</p>

<p>A bad way of structuring your project will involve storing different data that is not related to the same folder, such as the notification component and the authentication component in the root component folder:</p>

<pre><code class="language-javascript">+-- src/
|   +-- assets/
|       +-- logo.png
|       +-- userprofile.png
|   +-- components
|       +-- NotificationBar.vue
|       +-- LoginForm.vue
|       +-- DashboardInfo.vue
|       +-- AuthenticationModal.vue
|   +-- main.js</code></pre>

<p>So what we want to do is decouple the project based on business logic and concerns so that we have something like authentication module, product module, service module, and so on. This way we can make sure that anything concerning that particular feature is put in the module, making our code neater and navigating not so hard.</p>

<pre><code class="language-javascript">+-- modules/
|   +-- AuthModule/
|       +-- assets/
|           +-- userprofile.png
|       +-- Components/
|           +-- Authentication.vue
|           +-- login.vue
|   +-- NotificationModule
|       +-- assets/
|            +-- Alert.png
|       +-- Components/
|            +-- NotificationBar.vue
|   +-- ProductModule/</code></pre>

<h3 id="organizing-modules">Organizing Modules</h3>

<p>There are two ways you can organize your modules:</p>

<ol>
<li>Vue.js core modules,</li>
<li>App feature modules.</li>
</ol>

<p>The Vue.js core modules are here to facilitate your Vue.js development. Modules like the service module containing all the network requests needed by the company are kept in this core module and all corresponding network requests are made from here.</p>

<p>Modularizing your app according to features is a great way of making a better file structure in your app. This will allow separation of your concern and make sure that you are only working on the feature that you or your team is assigned to. Another advantage of modularizing according to feature is its maintainability and ability to avoid technical debt in the long term where there might need to be a rework on the app.</p>

<p>Now whenever there is a need to add, remove or change the state of a particular feature, all we need to do is to navigate to that feature and make changes without breaking the app. This method of modularization allows for efficient program development and easy debugging and modification in our application.</p>

<p>For example, a payout feature assigned to you and your team is a good time to implement a <code>payout</code> module that encapsulates all functionalities and data for the feature.</p>

<pre><code class="language-javascript">+-- modules/
|   +-- payout/
|       +-- index.js
|       +-- assets/
|       +-- Components/
|            +-- PayOut.vue
|            +-- UserInfo.vue
|       +-- store/
|            +-- index.js 
|            +-- actions.js
|            +-- mutations.js          
|       +-- Test/</code></pre>

<p>Based on our payout feature above, we have an <code>index.js</code> file to import and use plugins associated only with the payout module. The asset folder houses all the assets (images and styles) for the module. Our component folder contains components related to the payout feature. The store folder contains our actions, mutations, and getters used to manage the state of this feature. There is also a test folder to carry out testing for this feature.</p>

<h2 id="using-custom-directives">Using Custom Directives</h2>

<p>Directives in Vue.js are a way for us to tell Vue.js to do something or exhibit a certain behavior for us. Examples of directives are <code>v-if</code>, <code>v-model</code>, <code>v-for</code>, etc. In our Vue.js app, when we use something like v-model to tie data to an input in a form, we are giving the Vue.js code some certain instructions that are peculiar to Vue.js. But what if we want a particular action or behavior that our Vue.js provided directive doesn&rsquo;t allow us to do, what do we do then? We can create what we call custom directives.</p>

<h3 id="registering-custom-directives-and-directives-hooks">Registering Custom Directives And Directives Hooks</h3>

<p>We can go about registering directives in two ways:</p>

<ol>
<li><strong>Globally</strong><br />
In our <code>main.js</code> file.</li>
<li><strong>Locally</strong><br />
In our component.</li>
</ol>

<p>Hooks in directives are like methods that fire when a certain action happens in our directives. Like the <strong>created</strong> and <strong>mounted</strong> hook life cycle hooks, we are provided with hooks to use in our directives.</p>

<p>Let&rsquo;s say we are building an application and in one of our pages, we want the background color to always change each time we navigate to it. We are going to name this directive <code>colorChange</code>. We can achieve that with the help of a directive.</p>

<p>Our template looks something like this:</p>

<pre><code class="language-html">&lt;template&gt;
  &lt;div id="app" v-color-change&gt;
    &lt;HelloWorld msg="Hello Vue in CodeSandbox!"/&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre>

<p>We can see the custom directive above, but to make it work, in our <code>main.js</code> file we add:</p>

<div class="break-out">

<pre><code class="language-javascript">
// custom directive
Vue.directive("color-change", {
  bind: function (el) {
    const random = Math.floor(Math.random() * 900000) + 100000;
    el.style.backgroundColor = `#${random}`
  }
})</code></pre>

</div>

<p>The above Vue.js directive takes in the directive name as the first argument then an <code>Object</code> as the second argument that controls the behavior of the directives. <code>bind</code> is one of the hooks we talked about and will be called once the directive is bound to the element. It accepts the following arguments:</p>

<ul>
<li><code>el</code><br />
This is the element node we have attached the directive to.</li>
<li><code>binding</code><br />
It contains helpful properties that change the behavior of the directive.</li>
<li><code>vnode</code><br />
This is the virtual node of Vue.js.</li>
</ul>

<p>We have created a random set of  6-digit numbers so that we can use it in changing the hex code of our background color style.</p>

<h3 id="best-practices-when-writing-custom-directives">Best Practices When Writing Custom Directives</h3>

<p>We have created a custom directive for the above, but we need to take note of a few things. Apart from <code>el</code>, never modify hook arguments and make sure the arguments are read-only because the hook arguments are objects with native methods that can cause side effects if modified. If necessary, use the Vue.js dataset to share information among hooks.</p>

<p>If we are using the CLI build of Vue.js, custom directives should be in the <code>main.js</code> file so that all the <code>.vue</code> files can have access to it. Your directive name should be something that resonates with what that particular directive does, very descriptive about the directive functionality.</p>

<p>You can see and play more with the code in <a href="https://codesandbox.io/s/boring-paper-ox1mg?file=/src/main.js:87-293">this codesandbox</a> I have created. You can also <a href="https://vuejs.org/v2/guide/custom-directive.html">read more about this in the Vue docs</a>.</p>

<iframe src="https://codesandbox.io/embed/boring-paper-ox1mg?fontsize=14&hidenavigation=1&theme=dark"
     style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
     title="boring-paper-ox1mg"
     allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
     sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
   ></iframe>

<h2 id="controlling-updates">Controlling Updates</h2>

<p>Vue.js reactivity system is powerful in a way that it detects things that need updating and updates them without you as the developer doing anything. For example, re-rendering a page each time we navigate to it. At times the case can be different as we might find ourselves writing code that requires us to force an update.</p>

<p><strong>Note:</strong> <em>If you find yourself needing to force an update, which is a rare occasion, then you may need to really understand Vue&rsquo;s Reactivity and how to properly use props in communicating dynamic data.</em></p>

<h3 id="forcing-an-update-to-occur">Forcing An Update To Occur</h3>

<p>In most cases, when the value in the vue data object changes, the view automatically re-renders, but it’s not always like this. a classic case of our view, not re-rendering is when we are using a <code>v-for</code> in our template to loop over some data in the data object, and we do not add a <code>:key</code> value in the <code>v-for</code> loop.</p>

<pre><code class="language-javascript">&lt;div v-for="item in itemsArray" :key="item"&gt;</code></pre>

<p>This gives Vue.js a way to track each node&rsquo;s identity and re-render the view for any change.</p>

<p>A rare situation that can cause us to force an update is if we intentionally or accidentally set an array item with the index.</p>

<pre><code class="language-javascript">var app = new Vue({
  data: {
    items: ['1', '2']
  }
})
app.items[1] = '7' //vue does not notice any change</code></pre>

<p>There are different ways to force an update or re-render. Some are very bad practices like the use of <code>v-if</code> to re-render the page when it&rsquo;s <code>true</code>, and when false, the component disappears and no longer exists. This is bad practice because the template is never destroyed but just hidden till it can be re-used.</p>

<pre><code class="language-javascript">&lt;template&gt;
    &lt;div v-if="show"&gt;
       &lt;button @click="rerender"&gt;re-render&lt;/button&gt;
    &lt;/div&gt;
&lt;/template&gt;</code></pre>

<pre><code class="language-javascript">&lt;script&gt;
  export default {
    data() {
      return {
        show: true,
      };
    },
    methods: {
      rerender() {
        this.show= false;
        this.$nextTick(() =&gt; {
            this.show = true;
        });
      }
    }
  };
&lt;/script&gt;</code></pre>

<p>In the code above, the state of <code>show</code> is initially set to true, meaning our component is initially rendered. Then, when we click on the button, the <code>rerender(</code>) function is called and the state of <code>show</code> is set to <code>false</code>, and the component is no longer rendered. On the next tick, which is a single DOM update cycle, <code>show</code> is set to <code>true</code> and our component is rendered again. This is a very hacky way of re-rendering.</p>

<p>I would like to talk about two legit ways this can be  done:</p>

<ol>
<li>Vue’s <code>$forceUpdate</code>.</li>
<li>Key changing pattern.</li>
</ol>

<p><strong>Vue’s <code>$forceUpdate</code>:</strong> In the use of <code>$forceUpdate</code>, the child components do not render, only the Vue.js instance, the instance, and child components with slots.</p>

<p>Globally we can force the update:</p>

<pre><code class="language-javascript">import Vue from 'vue';
Vue.forceUpdate();</code></pre>

<p>And locally too:</p>

<pre><code class="language-javascript">export default {
  methods: {
    methodThatForcesUpdate() {
      this.$forceUpdate();
    }
  }
}</code></pre>

<p>Using the <strong>key changing pattern</strong> which is much better than the <code>$forceUpdate</code> method is another way to go about this. The reason behind the key changing pattern being better is that it allows Vue.js to know which component is tied to a specific data and when the key changes, it destroys the old component to create a new one, according to <a href="https://github.com/matthiasg">matthiasg</a> on this <a href="https://github.com/vuejs/Discussion/issues/356#issuecomment-336060875">Github issue</a> I ran into. You can use a <code>:key</code> attribute to let Vue.js know which component is attached to a specific piece of data. When the key changes, it causes Vue.js to destroy the old component and a new one is created.</p>

<pre><code class="language-javascript">&lt;template&gt;
  &lt;Child
    :key="key"
  /&gt;
&lt;/template&gt;

&lt;script&gt;
  export default {
    data() {
      return {
        key: 0,
      };
    },
    methods: {
      forceRerender() {
        this.key += 1;
      }
    }
  }
&lt;/script&gt;</code></pre>

<div class="partners__lead-place"></div>

<h2 id="third-party-libraries-and-optimization">Third Party Libraries And Optimization</h2>

<p>It is almost inevitable that we do not use third-party libraries in our apps. Third-party libraries can begin to be a problem if we turn a blind eye to it, increasing bundle size and slowing down our application.</p>

<p>I recently used the Vuetify component library in a project and checked to see that the overall bundle size was a whooping 500kb minified. Things like this can become a bottleneck in our application. You can check the bundle size of your app by using <code>webpack-bundle-analyzer</code>. You can install it by running:</p>

<pre><code class="language-bash">npm install --save-dev webpack-bundle-analyzer</code></pre>

<p>and include it in your webpack config file:</p>

<div class="break-out">

<pre><code class="language-javascript">const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}</code></pre>

</div>

<h3 id="good-practices-to-optimize-your-vue-app">Good Practices To Optimize Your Vue App</h3>

<ul>
<li>Our main bundle should only contain dependencies that are critical to our app, like <code>vue</code>, <code>vuex</code>. We should avoid putting libraries that are used in specific routes in our app in the main bundle.</li>
<li>When using component libraries, you can import individual components from the libraries, instead of importing everything. For example, vuetify:</li>
</ul>

<pre><code class="language-javascript">&lt;template&gt;
  &lt;v-app&gt;
    &lt;v-navigation-drawer app&gt;
    &lt;!-- --&gt;
  &lt;/v-navigation-drawer&gt;
  &lt;v-app-bar app&gt;
    &lt;!-- --&gt;
  &lt;/v-app-bar&gt;
 &lt;/v-app&gt;
&lt;/template&gt;
&lt;script&gt;
import { VApp, VNavigationDrawer, VAppBar } from 'vuetify/lib'

export default {
  components: {
    VApp,
    VNavigationDrawer,
    VAppBar,
  }
}
&lt;/script&gt;</code></pre>

<p>By doing the above we have reduced the bundle size and redundant code, only using the components we want to use in that particular route.</p>

<h2 id="making-early-decisions-to-use-vuex">Making Early Decisions To Use Vuex</h2>

<p>Often I have found myself wondering if I should start up a project with Vuex. Sometimes I just want to start a small side project and I start it up without Vuex to manage my state and communication using props begins to get messy.</p>

<p>So when should we use Vuex? To answer this, we need to consider:</p>

<ul>
<li>Size of the project,</li>
<li>The simplicity of the code,</li>
<li>Routing,</li>
<li>Dataset involved,</li>
<li>Components nesting.</li>
</ul>

<p>If your app begins to grow, it&rsquo;s only appropriate to include Vuex to manage the state in your application. If you&rsquo;re ever in doubt if you should use a state manager when starting your project, then just use it. However, <a href="https://vuejsdevelopers.com/2020/10/05/composition-api-vuex/">there is a talk</a> of the new Vue3 composition API being a replacement for vuex.</p>

<h3 id="how-vuex-should-be-set-up-for-large-applications">How Vuex Should Be Set Up For Large Applications</h3>

<p>We have four components in the vuex store:</p>

<ul>
<li><strong>State</strong>: Store data in our store.</li>
<li><strong>Getters</strong>: Retrieve state data.</li>
<li><strong>Mutations</strong>: Used to mutate state data.</li>
<li><strong>Action</strong>: Used to commit mutations.</li>
</ul>

<p>When we use the above in Vuex we should keep in mind that actions should always commit mutations no matter what. This allows our <a href="https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd?hl=en">devtools</a> to be able to track changes and revert to a particular period in our state and asynchronous operations or business logic should be carried out in the actions.</p>

<p>You can create a separate file for each of the Vuex components to look like this:</p>

<pre><code class="language-bash">├── services
├── main.js
└── store
    ├── index.js          
    ├── actions.js
    ├── mutations.js
    └── Getters.js
├── components</code></pre>

<h3 id="moduling-according-to-feature">Moduling According To Feature</h3>

<p>If our project is a very large project with a team, we can modularize our store according to app features. This is done especially when there are complex and large projects with many files and folders and we just want an organized way of handling the structuring of our app. We have to be careful the way we go about this, if not we can do more harm than good. A simple store modularized according to the feature looks like this:</p>

<pre><code class="language-bash">store/
   ├── index.js 
   └── modules/
       ├── cart
           ├── index.js          
           ├── actions.js
           ├── mutations.js       
       ├── product.js
       ├── login.js</code></pre>

<h3 id="good-practice-when-using-vuex-modules">Good Practice When Using Vuex Modules</h3>

<p>As the modules we have created become more complicated, it becomes harder to manually import and organize. It is advised that your modules have an <code>index.js</code> file at the root of your module, bringing the files altogether.</p>

<p>Make sure you have a standard naming pattern in your store as this will increase maintainability. You can use camelCase for naming the modules then a <code>.store.js</code> extension. Example: <code>CartData.store.js</code>.</p>

<pre><code class="language-javascript">modules/
       ├── cart.js
           ├── index.js   -> auto export module       
           ├── userProduct.store.js
           ├── userData.store.js</code></pre>

<p>Code related to business logic or async code should not run inside mutations because of its blocking behavior, instead, actions should be used. It is considered best practice not to directly access a state object. Instead, use the getter function because it can be mapped into any vue component using the <code>mapGetters</code> behaving like a computed property with the getters result cached based on its dependencies. Also, make sure each module is namespaced and not to access them using the global state scope.</p>

<div class="partners__lead-place"></div>

<h2 id="using-the-provide-inject-method-to-pass-data-around">Using The Provide/Inject Method To Pass Data Around</h2>

<p>Think of an app that has different components. We have the parent component and the parent component has many child components. From the image below, we see our Child component A, B, and D as top components, then we see Component E nested in component D and component F nested in component E. What if we have app data (like User Address), that we want to use in child Component A, C, and F, and this User address data is in our parent component.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2249dc06-8600-4526-a68d-668bf8953ffa/2-tools-practices-speed-up-vuejs-development-process.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="371"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2249dc06-8600-4526-a68d-668bf8953ffa/2-tools-practices-speed-up-vuejs-development-process.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2249dc06-8600-4526-a68d-668bf8953ffa/2-tools-practices-speed-up-vuejs-development-process.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2249dc06-8600-4526-a68d-668bf8953ffa/2-tools-practices-speed-up-vuejs-development-process.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2249dc06-8600-4526-a68d-668bf8953ffa/2-tools-practices-speed-up-vuejs-development-process.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2249dc06-8600-4526-a68d-668bf8953ffa/2-tools-practices-speed-up-vuejs-development-process.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2249dc06-8600-4526-a68d-668bf8953ffa/2-tools-practices-speed-up-vuejs-development-process.jpeg"
			
			sizes="100vw"
			alt="An image that shows how provide/inject is used to pass data down to the child component"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The architecture of parent-to-child communication. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2249dc06-8600-4526-a68d-668bf8953ffa/2-tools-practices-speed-up-vuejs-development-process.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To do this, we need to:</p>

<ul>
<li>Provide value in the parent component (Dependency provider).</li>
<li>Inject the value in component F (dependency consumer).</li>
</ul>

<p>In our parent component we provide the data:</p>

<pre><code class="language-javascript">app.component('parent-component', {
  data() {
    return {
      user: {name:"Uma Victor", address:"No 33 Rumukwurushi"}
    }
  },
  provide() {
    return {
     userAddress: this.user.address
    }
  },
  template: `
    ...
  `
})</code></pre>

<p>We use <code>provide</code> as a function by returning an object to access component instance properties.</p>

<p>In our <code>child-f</code> component, we have the following:</p>

<pre><code class="language-javascript">app.component('child-f', {
  inject: ['userAddress'],
  template: `
    &lt;h2&gt;Injected property: {{ this.userAddress }}&lt;/h2&gt;
  `
})</code></pre>

<p>However, we noticed that if we change our <code>user.address</code> to another address, the change won&rsquo;t be reflected in our injected value, this is because the data provided to the provide/inject are not reactive initially. We can fix this by passing a <code>reactive</code> object to <code>provide</code>. We have to assign a computed property to our user object.</p>

<pre><code class="language-javascript">app.component('parent-component', {
  data() {
    return {
      user: {name:"Uma Victor", address:"No 33 Rumukwurushi"}
    }
  },
  provide() {
    return {
     userAddress: Vue.computed(() =&gt; this.user)

    }
  },
  template: `
    ...
  `
})</code></pre>

<p>This pattern can be very useful and simpler than using Vuex.</p>

<p><em>However, with</em> <em>Vue3, and the recent upgrade, we can now use context providers, enabling us to share data between multiple components just like vuex.</em></p>

<h2 id="proper-use-of-props-for-form-components">Proper Use of Props For Form Components</h2>

<p>Building forms on the web is one of those things not everyone loves doing. Vue.js makes building excellent forms easy. To achieve this we need to know how to properly use props in our form components. In a traditional app where we have signup, logins, or product page we want to have consistent behavior and design. For example, the sign-in page below.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f1223a43-84cd-400d-bab7-632bff185ea7/3-tools-practices-speed-up-vuejs-development-process.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="449"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f1223a43-84cd-400d-bab7-632bff185ea7/3-tools-practices-speed-up-vuejs-development-process.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f1223a43-84cd-400d-bab7-632bff185ea7/3-tools-practices-speed-up-vuejs-development-process.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f1223a43-84cd-400d-bab7-632bff185ea7/3-tools-practices-speed-up-vuejs-development-process.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f1223a43-84cd-400d-bab7-632bff185ea7/3-tools-practices-speed-up-vuejs-development-process.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f1223a43-84cd-400d-bab7-632bff185ea7/3-tools-practices-speed-up-vuejs-development-process.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f1223a43-84cd-400d-bab7-632bff185ea7/3-tools-practices-speed-up-vuejs-development-process.png"
			
			sizes="100vw"
			alt="An image of a sign in form"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      A simple sign-in form. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f1223a43-84cd-400d-bab7-632bff185ea7/3-tools-practices-speed-up-vuejs-development-process.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>With the code:</p>

<pre><code class="language-javascript">&lt;template&gt;
  &lt;div class="form-group"&gt;
  &lt;form&gt;
    &lt;label for="email"&gt;Your Name&lt;/label&gt;
    &lt;input
      type="text"
      id="name"
      class="form-control"
      placeholder="name"
      v-model="userData.name"
    /&gt;
    &lt;label for="email"&gt;Your Email Address&lt;/label&gt;
    &lt;input
      type="text"
      id="email"
      class="form-control"
      placeholder="Email"
      v-model="userData.email"
    /&gt;
    &lt;label for="email"&gt;Your Password&lt;/label&gt;
    &lt;input
      type="text"
      id="password"
      class="form-control"
      placeholder="password"
      v-model="userData.password"
    /&gt;
  &lt;/form&gt;
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
    export default {
        data() {
            return {
                userData: {
                    name: '',
                    email: '',
                    password: ''
                }
            }
        },
    }
&lt;/script&gt;</code></pre>

<p>We will like to have a <code>BaseInput</code> component that we can use for the three form inputs above. Our <code>BaseInput</code> looks like this:</p>

<div class="break-out">

<pre><code class="language-javascript">&lt;template&gt;
 &lt;div&gt;
   &lt;label v-if="label"&gt;{{ label }}&lt;/label&gt;
   &lt;input type="email" @value="value" @input="updateInput" v-bind="$attrs"&gt;
 &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
    export default {
      props: {
        label: {
          type: String,
          default: ""
        },
        value: [String, Number]
      },
      methods: {
        updateInput(event) {
          this.$emit('input', event.target.value)
        }
      }
    }
&lt;/script&gt;</code></pre>

</div>

<p>We want our <code>BaseInput</code> to accept a <code>label</code> prop which is always a string, and if the Input has a label, we show it in our template as we can see above.</p>

<p>When we fill the form, the <code>updateInput</code> method is triggered. The <code>updateInput</code> method takes the input event as an argument and it emits an event with the name of Input, along with the payload <code>event.target.value</code> which is the name (John Doe) in the form:</p>

<div class="break-out">

<pre><code class="language-javascript">&lt;BaseInput label="Your Name" v-model="userData.name" placeholder="Name"/&gt;</code></pre>

</div>

<p>The <code>v-model</code> will be listening for the input event and then when it gets it, it sets our <code>userData.name</code> to the payload it got.</p>

<p>If we want to set a placeholder for an input, we might experience an error, this is because in vue2 attributes always attach themselves to the parent, so to fix this we set <code>inheritAttrs</code> to <code>false</code> and bind <code>attrs</code>.</p>

<pre><code class="language-javascript">&lt;script&gt;
    export default {
      inheritAttrs: false,
      props: {
        label: {
          type: String,
          default: ""
        },
        value: [String, Number]
      },
      methods: {
        updateInput(event) {
          this.$emit('input', event.target.value)
        }
      }
    }
&lt;/script&gt;</code></pre>

<p>To where we want the placeholder attribute to be. Our form page code looks like this now:</p>

<div class="break-out">

<pre><code class="language-javascript">&lt;template&gt;
  &lt;div class="form-group"&gt;
    &lt;form&gt;
      &lt;BaseInput label="Your Name" v-model="userData.name" placeholder="Name"/&gt;
      &lt;BaseInput label="Your Email Address" v-model="userData.email" placeholder="Email"/&gt;
      &lt;BaseInput label="Your Password" v-model="userData.password" placeholder="Password"/&gt;
    &lt;/form&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre>

</div>

<p>We finally have a standalone reusable form component. You can play with the code in the <a href="https://codesandbox.io/s/sharp-euclid-szjkw?file=/src/main.js">codesandbox I made</a>.</p>

<p><strong>Note:</strong> <em><code>$Attrs</code> in Vue3 now includes all of your listeners, style bindings, and classes.</em></p>

<h2 id="getting-familiar-with-vue-devtools">Getting Familiar With Vue Devtools</h2>

<p>Vue.js Devtools is a very powerful tool as it helps us effectively debug our application in real-time. It is most powerful when we use Vuex and we have to manage mutations and track changes in our app. Most Vue.js developers use devtools as an <a href="https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd">extension</a>, but we can also install it as a standalone app.</p>

<p><strong>Note:</strong> <em>The Vue.js devtools only work in the development mode of your build and won&rsquo;t work in production so other people can&rsquo;t use it to inspect your app.</em></p>

<h3 id="installing-devtools-as-a-standalone-app">Installing Devtools As a Standalone App</h3>

<p>You might be wondering why we would want to install a standalone app for devtools when we can use the browser extension for it? It is because when you install it as a standalone app locally, you can use it from any browser.</p>

<p>We install it:</p>

<pre><code class="language-bash">// Globally
npm install -g @vue/devtools
// or locally
npm install --save-dev @vue/devtools</code></pre>

<p>Once it&rsquo;s done installing, run:</p>

<pre><code class="language-bash">vue-devtools</code></pre>

<p>Then in our <code>index.html</code> file, located in the public folder in the root of our Vue.js application we add:</p>

<pre><code class="language-javascript">&lt;script src="https://localhost:8098"&gt;&lt;/script&gt;</code></pre>

<p>Once your app is reloaded, it will automatically connect.</p>

<h3 id="some-operations-we-can-do-with-vue-devtools">Some Operations We Can Do With Vue Devtools</h3>

<p>Here are some helpful operations you can do on Vue.js DevTools.</p>

<ul>
<li><strong>Dark Theme</strong><br />
In the new DevTools, there is now an option to set between light, dark, or contrast themes. You can do this by going to your global settings and selecting it.</li>
</ul>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c110e351-1608-419e-93e7-7c402d58b084/1-tools-practices-speed-up-vuejs-development-process.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c110e351-1608-419e-93e7-7c402d58b084/1-tools-practices-speed-up-vuejs-development-process.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c110e351-1608-419e-93e7-7c402d58b084/1-tools-practices-speed-up-vuejs-development-process.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c110e351-1608-419e-93e7-7c402d58b084/1-tools-practices-speed-up-vuejs-development-process.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c110e351-1608-419e-93e7-7c402d58b084/1-tools-practices-speed-up-vuejs-development-process.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c110e351-1608-419e-93e7-7c402d58b084/1-tools-practices-speed-up-vuejs-development-process.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c110e351-1608-419e-93e7-7c402d58b084/1-tools-practices-speed-up-vuejs-development-process.png"
			
			sizes="100vw"
			alt="Vue devtools in darkmode"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Vue devtools in darkmode. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c110e351-1608-419e-93e7-7c402d58b084/1-tools-practices-speed-up-vuejs-development-process.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ul>
<li><strong>Timeline</strong><br />
The new timeline in the devtools displays information about events that occur and it&rsquo;s arranged in chronological order. It is located next to the inspector and settings view.</li>
</ul>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/504f4566-f345-44cc-8cd0-79cd323d0d7e/4-tools-practices-speed-up-vuejs-development-process.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="387"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/504f4566-f345-44cc-8cd0-79cd323d0d7e/4-tools-practices-speed-up-vuejs-development-process.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/504f4566-f345-44cc-8cd0-79cd323d0d7e/4-tools-practices-speed-up-vuejs-development-process.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/504f4566-f345-44cc-8cd0-79cd323d0d7e/4-tools-practices-speed-up-vuejs-development-process.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/504f4566-f345-44cc-8cd0-79cd323d0d7e/4-tools-practices-speed-up-vuejs-development-process.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/504f4566-f345-44cc-8cd0-79cd323d0d7e/4-tools-practices-speed-up-vuejs-development-process.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/504f4566-f345-44cc-8cd0-79cd323d0d7e/4-tools-practices-speed-up-vuejs-development-process.png"
			
			sizes="100vw"
			alt="Vue devtools timeline"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Vue devtools timeline. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/504f4566-f345-44cc-8cd0-79cd323d0d7e/4-tools-practices-speed-up-vuejs-development-process.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ul>
<li><strong>Format component name</strong><br />
You can choose to display your component name in either camelCase or kebab-case.</li>
</ul>

<p>There are many other operations that you can utilize in the vue devtools. You can check out their <a href="https://headwayapp.co/vue-js-devtools-changelog">changelog</a>.</p>

<h2 id="tools-to-make-work-in-vue-easier">Tools to Make Work In Vue Easier</h2>

<p>When working with Vuejs, we might encounter some features we would love to implement, but it might take a lot of time to hard code or just a little bit difficult to implement. As professional developers, we add certain tools and helper libraries to make things easier and we would be looking at some of them.</p>

<h3 id="testing-libraries">Testing libraries</h3>

<p>Testing can play a crucial role when building large-scale applications. It helps us to avoid unnecessary bugs during development when working with a team. Let&rsquo;s look at the three types of testing we can carry out in our Vue application and their frameworks.</p>

<ul>
<li><strong>Component Testing</strong><br />
<a href="https://testing-library.com/docs/vue-testing-library/intro">Vue Testing Library</a>, <a href="https://vue-test-utils.vuejs.org/">Vue Test Utils</a>.</li>
<li><strong>Unit testing</strong><br />
<a href="https://jestjs.io/">Jest</a>, <a href="https://mochajs.org/">Mocha</a>.</li>
<li><strong>End to End Tests</strong><br />
<a href="https://nightwatchjs.org/">Nightwatch.js</a>, <a href="https://www.cypress.io/">Cypress</a>.</li>
</ul>

<h3 id="component-libraries">Component Libraries</h3>

<p>A component library is a set of reusable components we can use in our application to make UI development much faster and more consistent in our application. Like React and Angular, Vue has its own set of component libraries. Some of them include:</p>

<ul>
<li><a href="https://www.codeinwp.com/go/creative-tim-vue-material-kit/">Vue Material Kit</a><br />
A &ldquo;Badass&rdquo; Vue.js UI kit built upon material design. It contains more than 60+ handcrafted components.</li>
<li><a href="https://buefy.org/">Buefy</a><br />
A lightweight component library based on the Bulma CSS framework. If you are comfortable with SASS, you will have no problem using it.</li>
<li><a href="https://vuetifyjs.com/en/">Vuetify</a><br />
This is also a material design component framework with the availability of already made scaffolding for code, with a large community and regular updates</li>
<li><a href="https://quasar-framework.org/">Quasar</a><br />
My personal favorite, when it comes to the component framework. Quasar with its high performant frontend stack allows you to build cross-platform applications for Web, Mobile, and Desktop.</li>
</ul>

<h3 id="other-interesting-libraries">Other Interesting Libraries</h3>

<p>Other note worthy libraries are:</p>

<ul>
<li><a href="https://pqina.nl/filepond">FilePond</a><br />
This Vue.js library uploads any image you give it and optimizes those images with a silky-smooth experience.</li>
<li><a href="https://github.com/vuelidate/vuelidate">Vuelidate</a><br />
This library is very important when working with forms and you need a way to validate user inputs on the frontend. It is a simple and lightweight Model-based validation.</li>
<li><a href="https://www.npmjs.com/package/vue-clickaway">vue-Clickaway</a><br />
Vue doesn&rsquo;t have a native event listener to know when a user has clicked outside an element, for example, a dropdown, that&rsquo;s why <code>vue-clickaway</code> exists to detect click events.</li>
</ul>

<p><em>There are many more libraries out there. You can check out a plethora of them on</em> <em><a href="https://madewithvuejs.com/">madewithvuejs.com</a></em> <em>and</em> <em><a href="https://vuejsexamples.com/">vuejsexamples.com</a></em>.</p>

<h3 id="helpful-extensions-to-help-you-in-writing-vue">Helpful Extensions To Help You In Writing Vue</h3>

<p>Extensions are really helpful tools, which can make a big difference in your daily productivity when writing vuejs. During the time I have spent writing Vuejs code, I have found the following extensions Very helpful:</p>

<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=octref.vetur">Vetur</a><br />
This is the number one extension on my list. Saving me hours when writing Vuejs. It provides specific highlighting, snippets, Intellisense, debugging, and much more for Vue.js.</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=alefragnani.Bookmarks">Bookmarks</a><br />
This extension comes in very handy when working on a large project because you can mark and set a bookmark in places in your code and jump to that specific place when you want to.</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint">Eslint</a><br />
Eslint helps us to easily find coding errors by throwing a warning if we do something wrong in the code. It is advisable to use it in a prettier format.</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=mubaidr.vuejs-extension-pack">Vue.js Extension Pack</a><br />
This extension pack contains a collection of other extensions that will help in your Vue.js development like Prettier, Vetur, Night Owl, Etc.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>In this tutorial, we have looked at some tips and tools to help you become a better Vue developer. We started with some helpful insights on organizing our projects for scale and other great points to note and we rounded it up with Tools and extensions that make writing Vuejs so much easier.</p>

<p>Keep in mind that most of what is learned in this article is centered on Vue.js 2, to avoid misunderstandings.</p>

<h3 id="other-resources">Other Resources</h3>

<p>Here are some useful links you can check out if you want to dive deeper into some of the things we discussed above.</p>

<ul>
<li>“<a href="https://vuejs.org/v2/guide/custom-directive.html">Custom Directives</a>,” Official Docs</li>
<li>“<a href="https://vuejs.org/v2/guide/reactivity.html">Vue’s Reactivity</a>,” Official Docs</li>
<li>“<a href="https://devtools.vuejs.org/">Vue Devtools</a>,” website</li>
<li><a href="https://vuejsdevelopers.com/2020/10/05/composition-api-vuex/">Talk on Composition API vs Vuex</a></li>
<li><a href="https://www.smashingmagazine.com/2020/10/useful-tools-vue-javascript-web-development/">Useful tools vue javascript development</a> by Timi Omoyeni</li>
</ul>

<div class="partners__lead-place"></div>

<h3 id="further-reading">Further Reading</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2008/10/30-beautiful-vintageretro-photoshop-tutorials/">35 Beautiful Vintage and Retro Photoshop Tutorials</a></li>
<li><a href="https://www.smashingmagazine.com/2009/06/60-rare-and-unusual-vintage-signs/">60 Rare and Unusual Vintage Signs</a></li>
<li><a href="https://www.smashingmagazine.com/2008/10/26/retro-and-vintage-typography-showcase/">Vintage and Retro Typography Showcase</a></li>
<li><a href="https://www.smashingmagazine.com/2023/09/rediscovering-joy-happiness-design/">Rediscovering The Joy Of Design</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(yk, mrn)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Prince Chukwudire</author><title>How To Build A Geocoding App In Vue.js Using Mapbox</title><link>https://www.smashingmagazine.com/2021/06/building-geocoding-app-vue-mapbox/</link><pubDate>Mon, 07 Jun 2021 11:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2021/06/building-geocoding-app-vue-mapbox/</guid><description>In this guide, Prince Chukwudire will show you how to build a simple geocoding app from scratch, using Vue.js and Mapbox. He’ll cover the process from building the front-end scaffolding up to building a geocoder to handle forward geocoding and reverse geocoding. To get the most out of this guide, you’ll need a basic understanding of JavaScript and Vue.js and how to make API calls.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2021/06/building-geocoding-app-vue-mapbox/" />
              <title>How To Build A Geocoding App In Vue.js Using Mapbox</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>How To Build A Geocoding App In Vue.js Using Mapbox</h1>
                  
                    
                    <address>Prince Chukwudire</address>
                  
                  <time datetime="2021-06-07T11:00:00&#43;00:00" class="op-published">2021-06-07T11:00:00+00:00</time>
                  <time datetime="2021-06-07T11:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>Pinpoint accuracy and modularity are among the perks that make geocodes the perfect means of finding a particular location.</p>

<p>In this guide, we’ll build a <a href="https://github.com/smashingmagazine/Tutorials/tree/Geocoding-app-vue">simple geocoding app</a> from scratch, using Vue.js and Mapbox. We’ll cover the process from building the front-end scaffolding up to building a geocoder to handle forward geocoding and reverse geocoding. To get the most out of this guide, you’ll need a basic understanding of JavaScript and Vue.js and how to make API calls.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p><p>Meet <a data-instant href="/the-smashing-newsletter/"><strong>Smashing Email Newsletter</strong></a> with useful tips on front-end, design &amp; UX. Subscribe and <strong>get “Smart Interface Design Checklists”</strong> &mdash; a <strong>free PDF deck</strong> with 150+ questions to ask yourself when designing and building almost <em>anything</em>.</p><div><section class="nlbf"><form action="//smashingmagazine.us1.list-manage.com/subscribe/post?u=16b832d9ad4b28edf261f34df&amp;id=a1666656e0" method="post"><div class="nlbwrapper"><label for="mce-EMAIL-hp" class="sr-only">Your (smashing) email</label><div class="nlbgroup"><input type="email" name="EMAIL" class="nlbf-email" id="mce-EMAIL-hp" placeholder="Your email">
<input type="submit" value="Meow!" name="subscribe" class="nlbf-button"></div></div></form><style>.c-garfield-the-cat .nlbwrapper{margin-bottom: 0;}.nlbf{display:flex;padding-bottom:.25em;padding-top:.5em;text-align:center;letter-spacing:-.5px;color:#fff;font-size:1.15em}.nlbgroup:hover{box-shadow:0 1px 7px -5px rgba(50,50,93,.25),0 3px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025)}.nlbf .nlbf-button,.nlbf .nlbf-email{flex-grow:1;flex-shrink:0;width:auto;margin:0;padding:.75em 1em;border:0;border-radius:11px;background:#fff;font-size:1em;box-shadow:none}.promo-box .nlbf-button:focus,.promo-box input.nlbf-email:active,.promo-box input.nlbf-email:focus{box-shadow:none}.nlbf-button:-ms-input-placeholder,.nlbf-email:-ms-input-placeholder{color:#777;font-style:italic}.nlbf-button::-webkit-input-placeholder,.nlbf-email::-webkit-input-placeholder{color:#777;font-style:italic}.nlbf-button:-ms-input-placeholder,.nlbf-button::-moz-placeholder,.nlbf-button::placeholder,.nlbf-email:-ms-input-placeholder,.nlbf-email::-moz-placeholder,.nlbf-email::placeholder{color:#777;font-style:italic}.nlbf .nlbf-button{transition:all .2s ease-in-out;color:#fff;background-color:#0168b8;font-weight:700;box-shadow:0 1px 1px rgba(0,0,0,.3);width:100%;border:0;border-left:1px solid #ddd;flex:2;border-top-left-radius:0;border-bottom-left-radius:0}.nlbf .nlbf-email{border-top-right-radius:0;border-bottom-right-radius:0;width:100%;flex:4;min-width:150px}@media all and (max-width:650px){.nlbf .nlbgroup{flex-wrap:wrap;box-shadow:none}.nlbf .nlbf-button,.nlbf .nlbf-email{border-radius:11px;border-left:none}.nlbf .nlbf-email{box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);min-width:100%}.nlbf .nlbf-button{margin-top:1em;box-shadow:0 1px 1px rgba(0,0,0,.5)}}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus,.nlbf .nlbf-button:hover{cursor:pointer;color:#fff;background-color:#0168b8;border-color:#dadada;box-shadow:0 1px 1px rgba(0,0,0,.3)}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus{outline:0!important;text-shadow:1px 1px 1px rgba(0,0,0,.3);box-shadow:inset 0 3px 3px rgba(0,0,0,.3)}.nlbgroup{display:flex;box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);border-radius:11px;transition:box-shadow .2s ease-in-out}.nlbwrapper{display:flex;flex-direction:column;justify-content:center}.nlbf form{width:100%}.nlbf .nlbgroup{margin:0}.nlbcaption{font-size:.9em;line-height:1.5em;color:#fff;border-radius:11px;padding:.5em 1em;display:inline-block;background-color:#0067b859;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.wf-loaded-stage2 .nlbf .nlbf-button{font-family:Mija}.mts{margin-top: 5px !important;}.mbn{margin-bottom: 0 !important;}</style></section><p class="mts mbn"><small class="promo-box__footer mtm block grey"><em>Once a week. Useful tips on <a href="https://www.smashingmagazine.com/the-smashing-newsletter/">front-end &amp; UX</a>. Trusted by 207.000 friendly folks.</em></small></p></div></p>
</div>
</div>
<div class="feature-panel-right-col">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-firechat.svg"
    alt="Feature Panel"
    width="310"
    height="400"
/>

</div>

<p></div>
</aside>
</div></p>

<h2 id="what-is-geocoding">What Is Geocoding?</h2>

<p>Geocoding is the transformation of text-based locations to geographic coordinates (typically, longitude and latitude) that indicate a location in the world.</p>

<p>Geocoding is of two types: <strong>forward and reverse</strong>. Forward geocoding converts location texts to geographic coordinates, whereas reverse geocoding converts coordinates to location texts.</p>

<p>In other words, reverse geocoding turns 40.714224, -73.961452 into “277 Bedford Ave, Brooklyn”, and forward geocoding does the opposite, turning “277 Bedford Ave, Brooklyn” into 40.714224, -73.961452.</p>

<p>To give more insight, we will build a mini web app that uses an interactive web map with custom markers to display location coordinates, which we will subsequently decode to location texts.</p>

<p>Our app will have the following basic functions:</p>

<ul>
<li>give the user access to an interactive map display with a marker;</li>
<li>allow the user to move the marker at will, while displaying coordinates;</li>
<li>return a text-based location or location coordinates upon request by the user.</li>
</ul>

<h2 id="set-up-project-using-vue-cli">Set Up Project Using Vue CLI</h2>

<p>We’ll make use of the <a href="https://github.com/smashingmagazine/Tutorials/tree/geocoder/boilerplate">boilerplate found in this repository</a>. It contains a new project with the Vue CLI and <code>yarn</code> as a package manager. You’ll need to clone the repository. Ensure that you’re working from the <code>geocoder/boilerplate</code> branch.</p>

<h2 id="set-up-file-structure-of-application">Set Up File Structure Of Application</h2>

<p>Next, we will need to set up our project’s file structure. Rename the <code>Helloworld.vue</code> file in the component’s folder to <code>Index.vue</code>, and leave it blank for now. Go ahead and copy the following into the <code>App.vue</code> file:</p>

<pre><code class="language-javascript">&lt;template&gt;
  &lt;div id="app"&gt;
    &lt;!--Navbar Here --&gt;
    &lt;div&gt;
      &lt;nav&gt;
        &lt;div class="header"&gt;
          &lt;h3&gt;Geocoder&lt;/h3&gt;
        &lt;/div&gt;
      &lt;/nav&gt;
    &lt;/div&gt;
    &lt;!--Index Page Here --&gt;
    &lt;index /&gt;
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
import index from "./components/index.vue";
export default {
  name: "App",
  components: {
    index,
  },
};
&lt;/script&gt;</code></pre>

<p>Here, we’ve imported and then registered the recently renamed component locally. We’ve also added a navigation bar to lift our app’s aesthetics.</p>

<p>We need an <code>.env</code> file to load the environment variables. Go ahead and add one in the root of your project folder.</p>

<h2 id="install-required-packages-and-libraries">Install Required Packages And Libraries</h2>

<p>To kickstart the development process, we will need to install the required libraries. Here’s a list of the ones we’ll be using for this project:</p>

<ol>
<li><strong>Mapbox GL JS</strong><br />
This JavaScript library uses WebGL to render interactive maps from <a href="https://docs.mapbox.com/help/glossary/vector-tiles/">vector tiles</a> and <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/">Mapbox</a>.</li>
<li><strong>Mapbox-gl-geocoder</strong><br />
This geocoder control for Mapbox GL will help with our forward geocoding.</li>
<li><strong>Dotenv</strong><br />
We won’t have to install this because it comes preinstalled with the Vue CLI. It helps us to load environment variables from an <code>.env</code> file into <a href="https://nodejs.org/docs/latest/api/process.html#process_process_env"><code>process.env</code></a>. This way, we can keep our configurations separate from our code.</li>
<li><strong>Axios</strong><br />
This library will help us make HTTP requests.</li>
</ol>

<p>Install the packages in your CLI according to your preferred package manager. If you’re using Yarn, run the command below:</p>

<pre><code class="language-bash">cd geocoder && yarn add mapbox-gl @mapbox/mapbox-gl-geocoder axios</code></pre>

<p>If you’re using npm, run this:</p>

<pre><code class="language-bash">cd geocoder && npm i mapbox-gl @mapbox/mapbox-gl-geocoder axios --save</code></pre>

<p>We first had to enter the <code>geocoder</code> folder before running the installation command.</p>

<h2 id="scaffolding-the-front-end-with-vue-js">Scaffolding The Front End With Vue.js</h2>

<p>Let’s go ahead and create a layout for our app. We will need an element to house our map, a region to display the coordinates while listening to the marker’s movement on the map, and something to display the location when we call the reverse geocoding API. We can house all of this within a card component.</p>

<p>Copy the following into your <code>Index.vue</code> file:</p>

<pre><code class="language-javascript">&lt;template&gt;
  &lt;div class="main"&gt;
    &lt;div class="flex"&gt;
      &lt;!-- Map Display here --&gt;
      &lt;div class="map-holder"&gt;
        &lt;div id="map"&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;!-- Coordinates Display here --&gt;
      &lt;div class="dislpay-arena"&gt;
        &lt;div class="coordinates-header"&gt;
          &lt;h3&gt;Current Coordinates&lt;/h3&gt;
          &lt;p&gt;Latitude:&lt;/p&gt;
          &lt;p&gt;Longitude:&lt;/p&gt;
        &lt;/div&gt;
        &lt;div class="coordinates-header"&gt;
          &lt;h3&gt;Current Location&lt;/h3&gt;
          &lt;div class="form-group"&gt;
            &lt;input
              type="text"
              class="location-control"
              :value="location"
              readonly
            /&gt;
            &lt;button type="button" class="copy-btn"&gt;Copy&lt;/button&gt;
          &lt;/div&gt;
          &lt;button type="button" class="location-btn"&gt;Get Location&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre>

<p>To see what we currently have, start your development server. For Yarn:</p>

<pre><code class="language-bash">yarn serve</code></pre>

<p>Or for npm:</p>

<pre><code class="language-bash">npm run serve</code></pre>

<p>Our app should look like this now:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2db65f59-f1f9-4373-b2b1-8514d763b09d/1-building-geocoding-app-vue-mapbox.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="387"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2db65f59-f1f9-4373-b2b1-8514d763b09d/1-building-geocoding-app-vue-mapbox.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2db65f59-f1f9-4373-b2b1-8514d763b09d/1-building-geocoding-app-vue-mapbox.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2db65f59-f1f9-4373-b2b1-8514d763b09d/1-building-geocoding-app-vue-mapbox.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2db65f59-f1f9-4373-b2b1-8514d763b09d/1-building-geocoding-app-vue-mapbox.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2db65f59-f1f9-4373-b2b1-8514d763b09d/1-building-geocoding-app-vue-mapbox.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2db65f59-f1f9-4373-b2b1-8514d763b09d/1-building-geocoding-app-vue-mapbox.png"
			
			sizes="100vw"
			alt="Scaffold preview"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Geocoding app scaffold preview. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2db65f59-f1f9-4373-b2b1-8514d763b09d/1-building-geocoding-app-vue-mapbox.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The blank spot to the left looks off. It should house our map display. Let’s add that.</p>

<div class="partners__lead-place"></div>

<h2 id="interactive-map-display-with-mapbox">Interactive Map Display With Mapbox</h2>

<p>The first thing we need to do is gain access to the Mapbox GL and Geocoder libraries. We’ll start by importing the Mapbox GL and Geocoder libraries in the <code>Index.vue</code> file.</p>

<pre><code class="language-javascript">import axios from "axios";
import mapboxgl from "mapbox-gl";
import MapboxGeocoder from "@mapbox/mapbox-gl-geocoder";
import "@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css";</code></pre>

<p>Mapbox requires a unique <a href="https://docs.mapbox.com/help/glossary/access-token/">access token</a> to compute map vector tiles. <a href="https://docs.mapbox.com/help/glossary/access-token/">Get yours</a>, and add it as an environmental variable in your <code>.env</code> file.</p>

<pre><code class="language-javascript">.env</code></pre>

<pre><code class="language-javascript">VUE_APP_MAP_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</code></pre>

<p>We also need to define properties that will help with putting our map tiles together in our data instance. Add the following below the spot where we imported the libraries:</p>

<pre><code class="language-javascript">export default {
  data() {
    return {
      loading: false,
      location: "",
      access_token: process.env.VUE_APP_MAP_ACCESS_TOKEN,
      center: [0, 0],
      map: {},
    };
  },
}</code></pre>

<ul>
<li>The <code>location</code> property will be modeled to the input that we have in our scaffolding. We will use this to handle reverse geocoding (i.e. display a location from the coordinates).</li>
<li>The <code>center</code> property houses our coordinates (longitude and latitude). This is critical to putting our map tiles together, as we will see shortly.</li>
<li>The <code>access_token</code> property refers to our environmental variable, which we added earlier.</li>
<li>The <code>map</code> property serves as a constructor for our map component.</li>
</ul>

<p>Let’s proceed to create a method that plots our interactive map with our forward geocoder embedded in it. This method is our base function, serving as an intermediary between our component and Mapbox GL; we will call this method <code>createMap</code>. Add this below the data object:</p>

<pre><code class="language-javascript">mounted() {
  this.createMap()
},

methods: {
  async createMap() {
    try {
      mapboxgl.accessToken = this.access_token;
      this.map = new mapboxgl.Map({
        container: "map",
        style: "mapbox://styles/mapbox/streets-v11",
        center: this.center,
        zoom: 11,
      });

    } catch (err) {
      console.log("map error", err);
    }
  },
},
</code></pre>

<p>To create our map, we’ve specified a <code>container</code> that houses the map, a <code>style</code> property for our map’s display format, and a <code>center</code> property to house our coordinates. The <code>center</code> property is an array type and holds the longitude and latitude.</p>

<p>Mapbox GL JS initializes our map based on these parameters on the page and returns a <code>Map</code> object to us. The <code>Map</code> object refers to the map on our page, while exposing methods and properties that enable us to interact with the map. We’ve stored this returned object in our data instance, <code>this.map</code>.</p>

<h2 id="forward-geocoding-with-mapbox-geocoder">Forward Geocoding With Mapbox Geocoder</h2>

<p>Now, we will add the geocoder and custom marker. The geocoder handles forward geocoding by transforming text-based locations to coordinates. This will appear in the form of a search input box appended to our map.</p>

<p>Add the following below the <code>this.map</code> initialization that we have above:</p>

<pre><code class="language-javascript">let geocoder =  new MapboxGeocoder({
    accessToken: this.access_token,
    mapboxgl: mapboxgl,
    marker: false,
  });

this.map.addControl(geocoder);

geocoder.on("result", (e) => {
  const marker = new mapboxgl.Marker({
    draggable: true,
    color: "#D80739",
  })
    .setLngLat(e.result.center)
    .addTo(this.map);
  this.center = e.result.center;
  marker.on("dragend", (e) => {
    this.center = Object.values(e.target.getLngLat());
  });
});</code></pre>

<p>Here, we’ve first created a new instance of a geocoder using the <code>MapboxGeocoder</code> constructor. This initializes a geocoder based on the parameters provided and returns an object, exposed to methods and events. The <code>accessToken</code> property refers to our Mapbox access token, and <code>mapboxgl</code> refers to the <a href="https://docs.mapbox.com">map library</a> currently used.</p>

<p>Core to our app is the custom marker; the geocoder comes with one by default. This, however, wouldn’t give us all of the customization we need; hence, we’ve disabled it.</p>

<p>Moving along, we’ve passed our newly created geocoder as a parameter to the <code>addControl</code> method, exposed to us by our map object. <code>addControl</code> accepts a <code>control</code> as a parameter.</p>

<p>To create our custom marker, we’ve made use of an event exposed to us by our geocoder object. The <code>on</code> event listener enables us to subscribe to events that happen within the geocoder. It accepts various <a href="https://github.com/mapbox/mapbox-gl-geocoder/blob/master/API.md#on">events</a> as parameters. We’re listening to the <code>result</code> event, which is fired when an input is set.</p>

<p>In a nutshell, on <code>result</code>, our marker constructor creates a marker, based on the parameters we have provided (a draggable attribute and color, in this case). It returns an object, with which we use the <code>setLngLat</code> method to get our coordinates. We append the custom marker to our existing map using the <code>addTo</code> method. Finally, we update the <code>center</code> property in our instance with the new coordinates.</p>

<p>We also have to track the movement of our custom marker. We’ve achieved this by using the <code>dragend</code> event listener, and we updated our <code>center</code> property with the current coordinates.</p>

<p>Let’s update the template to display our interactive map and forward geocoder. Update the coordinates display section in our template with the following:</p>

<pre><code class="language-html">&lt;div class="coordinates-header"&gt;
  &lt;h3&gt;Current Coordinates&lt;/h3&gt;
  &lt;p&gt;Latitude: {{ center[0] }}&lt;/p&gt;
  &lt;p&gt;Longitude: {{ center[1] }}&lt;/p&gt;
&lt;/div&gt;
</code></pre>

<p>Remember how we always updated our <code>center</code> property following an event? We are displaying the coordinates here based on the current value.</p>

<p>To lift our app’s aesthetics, add the following CSS file in the <code>head</code> section of the <code>index.html</code> file. Put this file in the public folder.</p>

<div class="break-out">

<pre><code class="language-html">&lt;link href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.0/mapbox-gl.css" rel="stylesheet" /&gt;</code></pre>

</div>

<p>Our app should look like this now:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/94ec158a-9a69-4349-8676-38307bfda229/2-building-geocoding-app-vue-mapbox.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="389"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/94ec158a-9a69-4349-8676-38307bfda229/2-building-geocoding-app-vue-mapbox.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/94ec158a-9a69-4349-8676-38307bfda229/2-building-geocoding-app-vue-mapbox.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/94ec158a-9a69-4349-8676-38307bfda229/2-building-geocoding-app-vue-mapbox.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/94ec158a-9a69-4349-8676-38307bfda229/2-building-geocoding-app-vue-mapbox.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/94ec158a-9a69-4349-8676-38307bfda229/2-building-geocoding-app-vue-mapbox.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/94ec158a-9a69-4349-8676-38307bfda229/2-building-geocoding-app-vue-mapbox.png"
			
			sizes="100vw"
			alt="Forward geocoding preview"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Forward geocoding preview. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/94ec158a-9a69-4349-8676-38307bfda229/2-building-geocoding-app-vue-mapbox.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="reverse-geocode-location-with-mapbox-api">Reverse Geocode Location With Mapbox API</h2>

<p>Now, we will handle reverse geocoding our coordinates to text-based locations. Let’s write a method that handles that and trigger it with the <code>Get Location</code> button in our template.</p>

<p>Reverse geocoding in Mapbox is handled by the reverse geocoding API. This accepts <code>longitude</code>, <code>latitude</code>, and <code>access token</code> as request parameters. This call returns a response payload — typically, with various details. Our concern is the first object in the <code>features</code> array, where the reverse geocoded location is.</p>

<p>We’ll need to create a function that sends the <code>longitude</code>, <code>latitude</code> and <code>access_token</code> of the location we want to get to the Mapbox API. We need to send them in order to get the details of that location.</p>

<p>Finally, we need to update the <code>location</code> property in our instance with the value of the <code>place_name</code> key in the object.</p>

<p>Below the <code>createMap()</code> function, let’s add a new function that handles what we want. This is how it should look:</p>

<div class="break-out">

<pre><code class="language-javascript">async getLocation() {
  try {
    this.loading = true;
    const response = await axios.get(
      `https://api.mapbox.com/geocoding/v5/mapbox.places/${this.center[0]},${this.center[1]}.json?access_token=${this.access_token}`
    );
    this.loading = false;
    this.location = response.data.features[0].place_name;
  } catch (err) {
    this.loading = false;
    console.log(err);
  }
},
</code></pre>

</div>

<p>This function makes a <code>GET</code> request to the Mapbox API. The response contains <code>place_name</code> — the name of the selected location. We get this from the response and then set it as the value of <code>this.location</code>.</p>

<p>With that done, we need to edit and set up the button that will call this function we have created. We’ll make use of a <code>click</code> event listener — which will call the <code>getLocation</code> method when a user clicks on it. Go ahead and edit the button component to this.</p>

<pre><code class="language-javascript">&lt;button
  type="button"
  :disabled="loading"
  :class="{ disabled: loading }"
  class="location-btn"
  @click="getLocation"
&gt;
  Get Location
&lt;/button&gt;
</code></pre>

<p>As icing on the cake, let’s attach a function to copy the displayed location to the clipboard. Add this just below the <code>getLocation</code> function:</p>

<pre><code class="language-javascript">copyLocation() {
  if (this.location) {
    navigator.clipboard.writeText(this.location);
    alert("Location Copied")
  }
  return;
},
</code></pre>

<p>Update the <code>Copy</code> button component to trigger this:</p>

<pre><code class="language-html">&lt;button type="button" class="copy-btn" @click="copyLocation"&gt;</code></pre>

<h2 id="conclusion">Conclusion</h2>

<p>In this guide, we’ve looked at geocoding using Mapbox. We built a geocoding app that transforms text-based locations to coordinates, displaying the location on an interactive map, and that converts coordinates to text-based locations, according to the user’s request. This guide is just the beginning. A lot more could be achieved with the geocoding APIs, such as changing the presentation of the map using the various <a href="https://docs.mapbox.com/api/maps/styles/">map styles</a> provided by Mapbox.</p>

<ul>
<li><em>The source code is <a href="https://github.com/smashingmagazine/Tutorials/tree/Geocoding-app-vue">available on GitHub</a>.</em></li>
</ul>

<h3 id="resources">Resources</h3>

<ul>
<li>“<a href="https://docs.mapbox.com/api/search/geocoding/">Geocoding</a>”, Mapbox documentation</li>
<li>“<a href="https://docs.mapbox.com/api/maps/styles/">Styles</a>”, Mapbox documentation</li>
<li>“<a href="https://cli.vuejs.org/guide/mode-and-env.html#using-env-variables-in-client-side-code">Using Env Variables in Client-Side Code</a>”, in “Modes and Environment Variables”, Vue CLI</li>
</ul>

<div class="partners__lead-place"></div>

<h3 id="further-reading">Further Reading</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2024/06/uniting-web-native-apps-unknown-javascript-apis/">Uniting Web And Native Apps With 4 Unknown JavaScript APIs</a></li>
<li><a href="https://www.smashingmagazine.com/2023/06/build-server-side-rendered-svelte-apps-sveltekit/">How To Build Server-Side Rendered (SSR) Svelte Apps With SvelteKit</a></li>
<li><a href="https://www.smashingmagazine.com/2022/11/optimizing-vue-app/">Optimizing A Vue App</a></li>
<li><a href="https://www.smashingmagazine.com/2024/03/setting-persisting-color-scheme-preferences-css-javascript/">Setting And Persisting Color Scheme Preferences With CSS And A “Touch” Of JavaScript</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(ks, vf, yk, il, al, mrn)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Shawn Wildermuth</author><title>Managing Shared State In Vue 3</title><link>https://www.smashingmagazine.com/2021/06/managing-shared-state-vue3/</link><pubDate>Thu, 03 Jun 2021 10:30:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2021/06/managing-shared-state-vue3/</guid><description>Writing large-scale Vue applications can be a challenge. Using shared state in your Vue 3 applications can be a solution to reducing this complexity. There are a number common solutions to solving state. In this article, I will dive into the pros and cons of approaches like factories, shared objects, and using Vuex. I’ll also show you what is coming in Vuex 5 that might change how we all use shared state in Vue 3.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2021/06/managing-shared-state-vue3/" />
              <title>Managing Shared State In Vue 3</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Managing Shared State In Vue 3</h1>
                  
                    
                    <address>Shawn Wildermuth</address>
                  
                  <time datetime="2021-06-03T10:30:00&#43;00:00" class="op-published">2021-06-03T10:30:00+00:00</time>
                  <time datetime="2021-06-03T10:30:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>State can be hard. When we start a simple Vue project, it can be simple to just keep our working state on a particular component:</p>

<pre><code class="language-javascript">setup() {
  let books: Work[] = reactive([]);

  onMounted(async () =&gt; {
    // Call the API
    const response = await bookService.getScienceBooks();
    if (response.status === 200) {
      books.splice(0, books.length, ...response.data.works);
    }
  });

  return {
    books
  };
},
</code></pre>

<p>When your project is a single page of showing data (perhaps to sort or filter it), this can be compelling. But in this case, this component will get data on every request. What if you want to keep it around? That’s where state management comes into play. As network connections are often expensive and occasionally unreliable, it would be better to keep this state around as you navigate through an application.</p>

<p>Another issue is communicating between components. While you can use events and props to communicate with direct children-parents, handling simple situations like error handling and busy flags can be difficult when each of your views/pages are independent. For example, imagine that you had a top-level control was wired up to show error and loading animation:</p>

<pre><code class="language-javascript">// App.vue
&lt;template&gt;
  &lt;div class="container mx-auto bg-gray-100 p-1"&gt;
    &lt;router-link to="/"&gt;&lt;h1&gt;Bookcase&lt;/h1&gt;&lt;/router-link&gt;
    &lt;div class="alert" v-if="error"&gt;{{ error }}&lt;/div&gt;
    &lt;div class="alert bg-gray-200 text-gray-900" v-if="isBusy"&gt;
      Loading...
    &lt;/div&gt;
    &lt;router-view :key="$route.fullPath"&gt;&lt;/router-view&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>

<p>Without an effective way to handle this state, it might suggest a publish/subscribe system, but in fact sharing data is more straightforward in many cases. If want to have shared state, how do you go about it? Let’s look at some common ways to do this.</p>

<p><strong>Note</strong>: <em>You’ll find the code for this section in the “main” branch of the <a href="https://github.com/smashingmagazine/SmashingBookcase">example project on GitHub</a>.</em></p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Roll up your sleeves and <strong>boost your UX skills</strong>! Meet <strong><a data-instant href="https://smart-interface-design-patterns.com/">Smart Interface Design Patterns</a></strong>&nbsp;🍣, a 10h video library by Vitaly Friedman. <strong>100s of real-life examples</strong> and live UX training. <a href="https://www.youtube.com/watch?v=3mwZztmGgbE">Free preview</a>.</p>
<a data-instant href="https://smart-interface-design-patterns.com/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://smart-interface-design-patterns.com/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3155f571-450d-42f9-81b4-494aa9b52841/video-course-smart-interface-design-patterns-vitaly-friedman.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8c98e7f9-8e62-4c43-b833-fc6bf9fea0a9/video-course-smart-interface-design-patterns-vitaly-friedman.jpg"
    alt="Feature Panel"
    width="690"
    height="790"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="shared-state-in-vue-3">Shared State In Vue 3</h2>

<p>Since moving to Vue 3, I’ve migrated completely to using the Composition API. For the article, I’m also using TypeScript though that’s not required for examples I’m showing you. While you can share state any way you want, I’m going to show you several techniques that I find the most commonly used patterns. Each has it’s own pros and cons, so don’t take anything I talk about here as dogma.</p>

<p>The techniques include:</p>

<ul>
  <li><a href="#factories">Factories</a>,</li>
  <li><a href="#shared-singletons">Shared Singletons</a>,</li>
  <li><a href="#vuex4">Vuex 4</a>,</li>
  <li><a href="#vuex5">Vuex 5</a>.</li>
</ul>

<p><strong>Note</strong>: <em>Vuex 5, as of the writing of this article, it’s in the RFC (Request for Comments) stage so I want to get you ready for where Vuex is going, but right now there is not a working version of this option.</em></p>

<p>Let’s dig in&hellip;</p>

<h3 id="factories">Factories</h3>

<p><strong>Note</strong>: <em>The code for this section is in the “Factories” branch of the example project on <a href="https://github.com/smashingmagazine/SmashingBookcase/tree/Factories">GitHub</a>.</em></p>

<p>The factory pattern is just about creating an instance of the state you care about. In this pattern, you return a function that is much like the <strong>start</strong> function in the Composition API. You’d create a scope and build the components of what you’re looking for. For example:</p>

<pre><code class="language-javascript">export default function () {

  const books: Work[] = reactive([]);

  async function loadBooks(val: string) {
      const response = await bookService.getBooks(val, currentPage.value);
      if (response.status === 200) {
        books.splice(0, books.length, ...response.data.works);
      }
  }

  return {
    loadBooks,
    books
  };
}
</code></pre>

<p>You could ask for just the parts of the factory created objects you need like so:</p>

<pre><code class="language-javascript">// In Home.vue
  const { books, loadBooks } = BookFactory();
</code></pre>

<p>If we add an <code>isBusy</code> flag to show when the network request happens, the above code doesn’t change, but you could decide where you are going to show the <code>isBusy</code>:</p>

<pre><code class="language-javascript">export default function () {

  const books: Work[] = reactive([]);
  const isBusy = ref(false);

  async function loadBooks(val: string) {
    isBusy.value = true;
    const response = await bookService.getBooks(val, currentPage.value);
    if (response.status === 200) {
      books.splice(0, books.length, ...response.data.works);
    }
  }

  return {
    loadBooks,
    books,
    isBusy
  };
}
</code></pre>

<p>In another view (vue?) you could just ask for the isBusy flag without having to know about how the rest of the factory works:</p>

<pre><code class="language-javascript">// App.vue
export default defineComponent({
  setup() {
    const { isBusy } = BookFactory();
    return {
      isBusy
    }
  },
})
</code></pre>

<p>But you may have noticed an issue; every time we call the factory, we’re getting a new instance of all the objects. There are times when you want to have a factory return new instances, but in our case we’re talking about sharing the state, so we need to move the creation outside the factory:</p>

<pre><code class="language-javascript">const books: Work[] = reactive([]);
const isBusy = ref(false);

async function loadBooks(val: string) {
  isBusy.value = true;
  const response = await bookService.getBooks(val, currentPage.value);
  if (response.status === 200) {
    books.splice(0, books.length, ...response.data.works);
  }
}

export default function () {
 return {
    loadBooks,
    books,
    isBusy
  };
}
</code></pre>

<p>Now the factory is giving us a shared instance, or a singleton if you prefer. While this pattern works, it can be confusing to return a function that doesn’t create a new instance every time.</p>

<p>Because the underlying objects are marked as <code>const</code> you shouldn’t be able to replace them (and break the singleton nature). So this code should complain:</p>

<pre><code class="language-javascript">// In Home.vue
  const { books, loadBooks } = BookFactory();

  books = []; // Error, books is defined as const
</code></pre>

<p>So it can be important to make sure mutable state can be updated (e.g. using <code>books.splice()</code> instead of assigning the books).</p>

<p>Another way to handle this is to use shared instances.</p>

<h3 id="shared-instances">Shared Instances</h3>

<p><em>The code for this section is in the “SharedState” branch of the example project on <a href="https://github.com/smashingmagazine/SmashingBookcase/tree/SharedState">GitHub</a>.</em></p>

<p>If you’re going to be sharing state, might as well be clear about the fact that the state is a singleton. In this case, it can just be imported as a static object. For example, I like to create an object that can be imported as a reactive object:</p>

<pre><code class="language-javascript">export default reactive({

  books: new Array&lt;Work&gt;(),
  isBusy: false,

  async loadBooks() {
    this.isBusy = true;
    const response = await bookService.getBooks(this.currentTopic, this.currentPage);
    if (response.status === 200) {
      this.books.splice(0, this.books.length, ...response.data.works);
    }
    this.isBusy = false;
  }
});
</code></pre>

<p>In this case, you just import the object (which I’m calling a store in this example):</p>

<pre><code class="language-javascript">// Home.vue
import state from "@/state";

export default defineComponent({
  setup() {

    // ...

    onMounted(async () => {
      if (state.books.length === 0) state.loadBooks();
    });

    return {
      state,
      bookTopics,
    };
  },
});
</code></pre>

<p>Then it becomes easy to bind to the state:</p>

<pre><code class="language-javascript">&lt;!-- Home.vue --&gt;
&lt;div class="grid grid-cols-4"&gt;
  &lt;div
    v-for="book in state.books"
    :key="book.key"
    class="border bg-white border-grey-500 m-1 p-1"
  &gt;
  &lt;router-link :to="{ name: 'book', params: { id: book.key } }"&gt;
    &lt;BookInfo :book="book" /&gt;
  &lt;/router-link&gt;
&lt;/div&gt;
</code></pre>

<p>Like the other patterns, you get the benefit that you can share this instance between views:</p>

<pre><code class="language-javascript">// App.vue
import state from "@/state";

export default defineComponent({
  setup() {
    return {
      state
    };
  },
})
</code></pre>

<p>Then this can bind to what is the same object (whether it is a parent of the <code>Home.vue</code> or another page in the router):</p>

<pre><code class="language-javascript">&lt;!-- App.vue --&gt;
  &lt;div class="container mx-auto bg-gray-100 p-1"&gt;
    &lt;router-link to="/"&gt;&lt;h1&gt;Bookcase&lt;/h1&gt;&lt;/router-link&gt;
    &lt;div class="alert bg-gray-200 text-gray-900"   
         v-if="state.isBusy"&gt;Loading...&lt;/div&gt;
    &lt;router-view :key="$route.fullPath"&gt;&lt;/router-view&gt;
  &lt;/div&gt;
</code></pre>

<p>Whether you use the factory pattern or the shared instance, they both have a common issue: mutable state. You can have accidental side effects of bindings or code changing state when you don’t want them to. In a trivial example like I’m using here, it isn’t complex enough to worry about. But as you’re building larger and larger apps, you will want to think about state mutation more carefully. That’s where Vuex can come to the rescue.</p>

<div class="partners__lead-place"></div>

<h3 id="vuex-4">Vuex 4</h3>

<p><em>The code for this section is in the “Vuex4” branch of the example project on <a href="https://github.com/smashingmagazine/SmashingBookcase/tree/Vuex4">GitHub</a>.</em></p>

<p>Vuex is state manager for Vue. It was built by the core team though it is managed as a separate project. The purpose of Vuex is to separate the state from the actions you want to do to the state. All changes of state has to go through Vuex which means it is more complex, but you get protection from accidental state change.</p>

<p>The idea of Vuex is to provide a predictable flow of state management. Views flow to Actions which, in turn, use Mutations to change State which, in turn, updates the View. By limiting the flow of state change, you should have fewer side effects that change the state of your applications; therefore be easier to build larger applications. Vuex has a learning curve, but with that complexity you get predictability.</p>

<p>Additionally, Vuex does support development-time tools (via the Vue Tools) to work with the state management including a feature called time-travel. This allows you to view a history of the state and move back and forward to see how it affects the application.</p>

<p>There are times, too, when Vuex is important too.</p>

<p>To add it to your Vue 3 project, you can either add the package to the project:</p>

<pre><code class="language-javascript">&gt; npm i vuex
</code></pre>

<p>Or, alternatively you can add it by using the Vue CLI:</p>

<pre><code class="language-javascript">&gt; vue add vuex
</code></pre>

<p>By using the CLI, it will create a starting point for your Vuex store, otherwise you’ll need to wire it up manually to the project. Let’s walk through how this works.</p>

<p>First, you’ll need a state object that is created with Vuex’s createStore function:</p>

<pre><code class="language-javascript">import { createStore } from 'vuex'

export default createStore({
  state: {},
  mutations: {},
  actions: {},
  getters: {}
});
</code></pre>

<p>As you can see, the store requires several properties to be defined. State is just a list of the data you want to give your application access to:</p>

<pre><code class="language-javascript">import { createStore } from 'vuex'

export default createStore({
  state: {
    books: [],
    isBusy: false
  },
  mutations: {},
  actions: {}
});
</code></pre>

<p>Note that the state shouldn’t use <strong>ref</strong> or <strong>reactive</strong> wrappers. This data is the same kind of share data that we used with Shared Instances or Factories. This store will be a singleton in your application, therefore the data in state is also going to be shared.</p>

<p>Next, let’s look at actions. Actions are operations that you want to enable that involve the state. For example:</p>

<pre><code class="language-javascript">  actions: {
    async loadBooks(store) {
      const response = await bookService.getBooks(store.state.currentTopic,
      if (response.status === 200) {
        // ...
      }
    }
  },
</code></pre>

<p>Actions are passed an instance of the store so that you can get at the state and other operations. Normally, we’d destructure just the parts we need:</p>

<pre><code class="language-javascript">  actions: {
    async loadBooks({ state }) {
      const response = await bookService.getBooks(state.currentTopic,
      if (response.status === 200) {
        // ...
      }
    }
  },
</code></pre>

<p>The last piece of this are Mutations. Mutations are functions that can mutate state. Only mutations can affect state. So, for this example, we need mutations that change change the state:</p>

<pre><code class="language-javascript">  mutations: {
    setBusy: (state) =&gt; state.isBusy = true,
    clearBusy: (state) =&gt; state.isBusy = false,
    setBooks(state, books) {
      state.books.splice(0, state.books.length, ...books);
    }
 },
</code></pre>

<p>Mutation functions always pass in the state object so that you can mutate that state. In the first two examples, you can see that we’re explicitly setting the state. But in the third example, we’re passing in the state to set. Mutations always take two parameters: state and the argument when calling the mutation.</p>

<p>To call a mutation, you’d use the <strong>commit</strong> function on the store. In our case, I’ll just add it to the destructuring:</p>

<pre><code class="language-javascript">  actions: {
    async loadBooks({ state, commit }) {
      commit("setBusy");
      const response = await bookService.getBooks(state.currentTopic, 
      if (response.status === 200) {
        commit("setBooks", response.data);
      }
      commit("clearBusy");
    }
  },
</code></pre>

<p>What you’ll see here is how <strong>commit</strong> requires the name of the action. There are tricks to make this not just use magic strings, but I’m going to skip that for now. This use of magic strings is one of the limitations of using Vuex.</p>

<p>While using commit may seem like an unnecessary wrapper, remember that Vuex is not going to let you mutate state except inside the mutation, therefore only calls through <strong>commit</strong> will.</p>

<p>You can also see that the call to <strong>setBooks</strong> takes a second argument. This is the second argument that is calling the mutation. If you were to need more information, you’d need to pack it into a single argument (another limitation of Vuex currently). Assuming you needed to insert a book into the books list, you could call it like this:</p>

<pre><code class="language-javascript">commit("insertBook", { book, place: 4 }); // object, tuple, etc.
</code></pre>

<p>Then you could just destructure into the pieces you need:</p>

<pre><code class="language-javascript">mutations: {
  insertBook(state, { book, place }) =&gt; // ...    
}
</code></pre>

<p>Is this elegant? Not really, but it works.</p>

<p>Now that we have our action working with mutations, we need to be able to use the Vuex store in our code. There are really two ways to get at the store. First, by registering the store with application (e.g. main.ts/js), you’ll have access to a centralized store that you have access to everywhere in your application:</p>

<pre><code class="language-javascript">// main.ts
import store from './store'

createApp(App)
  .use(store)
  .use(router)
  .mount('#app')
</code></pre>

<p>Note that this isn’t adding Vuex, but your actual store that you’re creating. Once this is added, you can just call <code>useStore</code> to get the store object:</p>

<pre><code class="language-javascript">import { useStore } from "vuex";

export default defineComponent({
  components: {
    BookInfo,
  },
  setup() {
    const store = useStore();
    const books = computed(() =&gt; store.state.books);
    // ...
  </code></pre>

<p>This works fine, but I prefer to just import the store directly:</p>

<pre><code class="language-javascript">import store from "@/store";

export default defineComponent({
  components: {
    BookInfo,
  },
  setup() {
    const books = computed(() =&gt; store.state.books);
    // ...
  </code></pre>

<p>Now that you have access to the store object, how do you use it? For state, you’ll need to wrap them with computed functions so that changes will be propagated to your bindings:</p>

<pre><code class="language-javascript">export default defineComponent({
  setup() {

    const books = computed(() =&gt; store.state.books);

    return {
      books
    };
  },
});
</code></pre>

<p>To call actions, you will need to call the <strong>dispatch</strong> method:</p>

<pre><code class="language-javascript">export default defineComponent({
  setup() {

    const books = computed(() =&gt; store.state.books);

    onMounted(async () =&gt; await store.dispatch("loadBooks"));

    return {
      books
    };
  },
});
</code></pre>

<p>Actions can have parameters that you add after the name of the method. Lastly, to change state, you’ll need to call commit just like we did inside the Actions. For example, I have a paging property in the store, and then I can change the state with <strong>commit</strong>:</p>

<pre><code class="language-javascript">const incrementPage = () =&gt;
  store.commit("setPage", store.state.currentPage + 1);
const decrementPage = () =&gt;
  store.commit("setPage", store.state.currentPage - 1);
</code></pre>

<p>Note, that calling it like this would throw an error (because you can’t change state manually):</p>

<pre><code class="language-javascript">const incrementPage = () =&gt; store.state.currentPage++;
  const decrementPage = () =&gt; store.state.currentPage--;
</code></pre>

<p>This is the real power here, we’d want control where state is changed and not have side effects that produce errors further down the line in development.</p>

<p>You may be overwhelmed with number of moving pieces in Vuex, but it can really help manage state in larger, more complex projects. I would not say you need it in every case, but there will be large projects where it helps you overall.</p>

<p>The big problem with Vuex 4 is that working with it in a TypeScript project leaves a lot to be desired. You can certainly make TypeScript types to help development and builds, but it requires a lot of moving pieces.</p>

<p>That’s where Vuex 5 is meant to simplify how Vuex works in TypeScript (and in JavaScript projects in general). Let’s see how that will work once it’s released next.</p>

<h3 id="vuex-5">Vuex 5</h3>

<p><strong>Note</strong>: <em>The code for this section is in the “Vuex5” branch of the example project on <a href="https://github.com/smashingmagazine/SmashingBookcase/tree/Vuex5">GitHub</a>.</em></p>

<p>At the time of this article, Vuex 5 isn’t real. It’s a RFC (Request for Comments). It’s a plan. It’s a starting point for discussion. So a lot of what I may explain here likely will change somewhat. But to prepare you for the change in Vuex, I wanted to give you a view of where it’s going. Because of this the code associated with this example doesn’t build.</p>

<p>The basic concepts of how Vuex works have been somewhat unchanged since it’s inception. With the introduction of Vue 3, Vuex 4 was created to mostly allow Vuex to work in new projects. But the team is trying to look at the real pain-points with Vuex and solve them. To this end they are planning some important changes:</p>

<ul>
<li>No more mutations: actions can mutate state (and possibly anyone).</li>
<li>Better TypeScript support.</li>
<li>Better multi-store functionality.</li>
</ul>

<p>So how would this work? Let’s start with creating the store:</p>

<pre><code class="language-javascript">export default createStore({
  key: 'bookStore',
  state: () =&gt; ({
    isBusy: false,
    books: new Array&lt;Work&gt;()
  }),
  actions: {
    async loadBooks() {
      try {
        this.isBusy = true;
        const response = await bookService.getBooks();
        if (response.status === 200) {
          this.books = response.data.works;
        }
      } finally {
        this.isBusy = false;
      }
    }
  },
  getters: {
    findBook(key: string): Work | undefined {
      return this.books.find(b =&gt; b.key === key);
    }
  }
});
</code></pre>

<p>First change to see is that every store now needs it own key. This is to allow you to retrieve multiple stores. Next you’ll notice that the state object is now a factory (e.g. returns from a function, not created on parsing). And there is no mutations section any more. Lastly, inside the actions, you can see we’re accessing state as just properties on the <code>this</code> pointer. No more having to pass in state and commit to actions. This helps not only in simplifying development, but also makes it easier to infer types for TypeScript.</p>

<p>To register Vuex into your application, you’ll register Vuex instead of your global store:</p>

<pre><code class="language-javascript">import { createVuex } from 'vuex'

createApp(App)
  .use(createVuex())
  .use(router)
  .mount('#app')
</code></pre>

<p>Finally, to use the store, you’ll import the store then create an instance of it:</p>

<pre><code class="language-javascript">import bookStore from "@/store";

export default defineComponent({
  components: {
    BookInfo,
  },
  setup() {
    const store = bookStore(); // Generate the wrapper
    // ...
  </code></pre>

<p>Notice that what is returned from the store is a factory object that returns thsi instance of the store, no matter how many times you call the factory. The returned object is just an object with the actions, state and getters as first class citizens (with type information):</p>

<pre><code class="language-javascript">onMounted(async () =&gt; await store.loadBooks());

const incrementPage = () =&gt; store.currentPage++;
const decrementPage = () =&gt; store.currentPage--;
</code></pre>

<p>What you’ll see here is that state (e.g. <code>currentPage</code>) are just simple properties. And actions (e.g. <code>loadBooks</code>) are just functions. The fact that you’re using a store here is a side effect. You can treat the Vuex object as just an object and go about your work. This is a significant improvement in the API.</p>

<p>Another change that’s important to point out is that you could also generate your store using a Composition API-like syntax:</p>

<pre><code class="language-javascript">export default defineStore("another", () =&gt; {

  // State
  const isBusy = ref(false);
  const books = reactive(new Array&gl;Work&gt;());

  // Actions
  async function loadBooks() {
    try {
      this.isBusy = true;
      const response = await bookService.getBooks(this.currentTopic, this.currentPage);
      if (response.status === 200) {
        this.books = response.data.works;
      }
    } finally {
      this.isBusy = false;
    }
  }

  findBook(key: string): Work | undefined {
    return this.books.find(b =&gt; b.key === key);
  }

  // Getters
  const bookCount = computed(() =&gt; this.books.length);

  return {
    isBusy,
    books,
    loadBooks,
    findBook,
    bookCount
  }
});
</code></pre>

<p>This allows you to build your Vuex object just like you would your views with the Composition API and arguably it’s simpler.</p>

<p>One main drawback in this new design is that you lose the non-mutability of the state. There are discussions happening around being able to enable this (for development only, just like Vuex 4) but there isn’t consensus how important this is. I personally think it’s a key benefit for Vuex, but we’ll have to see how this plays out.</p>

<h2 id="where-are-we">Where Are We?</h2>

<p>Managing shared state in single page applications is a crucial part of development for most apps. Having a game plan on how you want to go about it in Vue is an important step in designing your solution. In this article, I’ve shown you several patterns for managing shared state including what’s coming for Vuex 5. Hopefully you’ll now have the knowledge to make the right decision for you own projects.</p>

<div class="partners__lead-place"></div>

<h3 id="further-reading">Further Reading</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2023/03/building-complex-forms-vue/">Building Complex Forms In Vue</a></li>
<li><a href="https://www.smashingmagazine.com/2023/05/big-tooling-upgrades-large-organizations/">How To Deal With Big Tooling Upgrades In Large Organizations</a></li>
<li><a href="https://www.smashingmagazine.com/2024/05/netlify-platform-primitives/">The Era Of Platform Primitives Is Finally Here</a></li>
<li><a href="https://www.smashingmagazine.com/2023/05/safest-way-hide-api-keys-react/">The Safest Way To Hide Your API Keys When Using React</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(vf, yk, il, mrn)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Deven Rathore</author><title>Building A Video Streaming App With Nuxt.js, Node And Express</title><link>https://www.smashingmagazine.com/2021/04/building-video-streaming-app-nuxtjs-node-express/</link><pubDate>Tue, 13 Apr 2021 14:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2021/04/building-video-streaming-app-nuxtjs-node-express/</guid><description>Instead of sending the entire video at once, a video is sent as a set of smaller chunks that make up the full video. This explains why videos buffer when watching a video on slow broadband because it only plays the chunks it has received and tries to load more. In this article, Deven Rathore will build a video streaming app using Nuxt.js and Node.js. Specifically, you’ll build a server-side Node.js app that will handle fetching and streaming videos, generating thumbnails for your videos, and serving captions and subtitles.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2021/04/building-video-streaming-app-nuxtjs-node-express/" />
              <title>Building A Video Streaming App With Nuxt.js, Node And Express</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Building A Video Streaming App With Nuxt.js, Node And Express</h1>
                  
                    
                    <address>Deven Rathore</address>
                  
                  <time datetime="2021-04-13T14:00:00&#43;00:00" class="op-published">2021-04-13T14:00:00+00:00</time>
                  <time datetime="2021-04-13T14:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>Videos work with streams. This means that instead of sending the entire video at once, a video is sent as a set of smaller chunks that make up the full video. This explains why videos buffer when watching a video on slow broadband because it only plays the chunks it has received and tries to load more.</p>

<p>This article is for developers who are willing to learn a new technology by building an actual project: a video streaming app with <a href="https://nodejs.org/en/">Node.js</a> as the backend and <a href="https://nuxtjs.org/">Nuxt.js</a> as the client.</p>

<ul>
<li><strong>Node.js</strong> is a runtime used for building fast and scalable applications. We will use it to handle fetching and streaming videos, generating thumbnails for videos, and serving captions and subtitles for videos.</li>
<li><strong>Nuxt.js</strong> is a Vue.js framework that helps us build server-rendered Vue.js applications easily. We will consume our API for the videos and this application will have two views: a listing of available videos and a player view for each video.</li>
</ul>

<h2 id="prerequisities">Prerequisities</h2>

<ul>
<li>An understanding of HTML, CSS, JavaScript, Node/Express, and Vue.</li>
<li>A text editor (e.g. VS Code).</li>
<li>A web browser (e.g. Chrome, Firefox).</li>
<li><a href="https://www.ffmpeg.org/download.html">FFmpeg</a> installed on your workstation.</li>
<li><a href="https://nodejs.org/en/download/">Node.js</a>. <a href="https://github.com/nvm-sh/nvm">nvm</a>.</li>
<li>You can <a href="https://github.com/smashingmagazine/Nuxt-Node-video-streaming">get the source code on GitHub</a>.</li>
</ul>

<h2 id="setting-up-our-application">Setting Up Our Application</h2>

<p>In this application, we will build the routes to make requests from the frontend:</p>

<ul>
<li><code>videos</code> route to get a list of videos and their data.</li>
<li>a route to fetch only one video from our list of videos.</li>
<li><code>streaming</code> route to stream the videos.</li>
<li><code>captions</code> route to add captions to the videos we are streaming.</li>
</ul>

<p>After our routes have been created, we’ll scaffold our <code>Nuxt</code> frontend, where we’ll create the <code>Home</code> and dynamic <code>player</code> page. Then we request our <code>videos</code> route to fill the home page with the video data, another request to stream the videos on our <code>player</code> page, and finally a request to serve the caption files to be used by the videos.</p>

<p>To set up our application, we create our project directory,</p>

<pre><code class="language-bash">mkdir streaming-app</code></pre>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="/printed-books/touch-design-for-mobile-interfaces/">Touch Design for Mobile Interfaces</a></strong>, Steven Hoober’s brand-new guide on <strong>designing for mobile</strong> with proven, universal, human-centric guidelines. <strong>400 pages</strong>, jam-packed with in-depth user research and <strong>best practices</strong>.</p>
<a data-instant href="https://www.smashingmagazine.com/printed-books/touch-design-for-mobile-interfaces/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://www.smashingmagazine.com/printed-books/touch-design-for-mobile-interfaces/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/14bcab88-b622-47f6-a51d-76b0aa003597/touch-design-book-shop-opt.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b14658fc-bb2d-41a6-8d1a-70eaaf1b8ec8/touch-design-book-shop-opt.png"
    alt="Feature Panel"
    width="480"
    height="697"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="setting-up-our-server">Setting Up Our Server</h2>

<p>In our <code>streaming-app</code> directory, we create a folder named <code>backend</code>.</p>

<pre><code class="language-bash">cd streaming-app
mkdir backend</code></pre>

<p>In our backend folder, we initialize a <code>package.json</code> file to store information about our server project.</p>

<pre><code class="language-bash">cd backend
npm init -y</code></pre>

<p>we need to install the following packages to build our app.</p>

<ul>
<li><code>nodemon</code> automatically restarts our server when we make changes.</li>
<li><code>express</code> gives us a nice interface to handle routes.</li>
<li><code>cors</code> will allow us to make cross-origin requests since our client and server will be running on different ports.</li>
</ul>

<p>In our backend directory, we create a folder <code>assets</code> to hold our videos for streaming.</p>

<pre><code class="language-bash"> mkdir assets</code></pre>

<p>Copy a <code>.mp4</code> file into the assets folder, and name it <code>video1</code>. You can use  <code>.mp4</code> short sample videos that can be found on <a href="https://github.com/Dunebook/Nuxt-Node-video-streaming/tree/main/backend/assets">Github Repo</a>.</p>

<p>Create an <code>app.js</code> file and add the necessary packages for our app.</p>

<pre><code class="language-javascript">const express = require('express');
const fs = require('fs');
const cors = require('cors');
const path = require('path');
const app = express();
app.use(cors())</code></pre>

<p>The <code>fs</code> module is used to read and write into files easily on our server, while the <code>path</code> module provides a way of working with directories and file paths.</p>

<p>Now we create a <code>./video</code> route. When requested, it will send a video file back to the client.</p>

<pre><code class="language-javascript">// add after 'const app = express();'

app.get('/video', (req, res) =&gt; {
    res.sendFile('assets/video1.mp4', { root: __dirname });
});</code></pre>

<p>This route serves the <code>video1.mp4</code> video file when requested. We then listen to our server at port <code>3000</code>.</p>

<pre><code class="language-javascript">// add to end of app.js file

app.listen(5000, () =&gt; {
    console.log('Listening on port 5000!')
});</code></pre>

<p>A script is added in the <code>package.json</code> file to start our server using nodemon.</p>

<pre><code class="language-javascript">
"scripts": {
    "start": "nodemon app.js"
  },</code></pre>

<p>Then on your terminal run:</p>

<pre><code class="language-bash">npm run start</code></pre>

<p>If you see the message <code>Listening on port 3000!</code> in the terminal, then the server is working correctly. Navigate to <a href="https://localhost:5000/video">https://localhost:5000/video</a> in your browser and you should see the video playing.</p>

<h2 id="requests-to-be-handled-by-the-frontend">Requests To Be Handled By The Frontend</h2>

<p>Below are the requests that we will make to the backend from our frontend that we need the server to handle.</p>

<ul>
<li><code>/videos</code><br />
Returns an array of video mockup data that will be used to populate the list of videos on the <code>Home</code> page in our frontend.</li>
<li><code>/video/:id/data</code><br />
Returns metadata for a single video. Used by the <code>Player</code> page in our frontend.</li>
<li><code>/video/:id</code><br />
Streams a video with a given ID. Used by the <code>Player</code> page.</li>
</ul>

<p>Let’s create the routes.</p>

<div class="partners__lead-place"></div>

<h2 id="return-mockup-data-for-list-of-videos">Return Mockup Data For List Of Videos</h2>

<p>For this demo application, we’ll create an <strong>array of objects</strong> that will hold the metadata and send that to the frontend when requested. In a real application, you would probably be reading the data from a database, which would then be used to generate an array like this. For simplicity&rsquo;s sake, we won’t be doing that in this tutorial.</p>

<p>In our backend folder create a file <code>mockdata.js</code> and populate it with metadata for our list of videos.</p>

<div class="break-out">

<pre><code class="language-javascript">const allVideos = [
    {
        id: "tom and jerry",
        poster: 'https://image.tmdb.org/t/p/w500/fev8UFNFFYsD5q7AcYS8LyTzqwl.jpg',
        duration: '3 mins',
        name: 'Tom & Jerry'
    },
    {
        id: "soul",
        poster: 'https://image.tmdb.org/t/p/w500/kf456ZqeC45XTvo6W9pW5clYKfQ.jpg',
        duration: '4 mins',
        name: 'Soul'
    },
    {
        id: "outside the wire",
        poster: 'https://image.tmdb.org/t/p/w500/lOSdUkGQmbAl5JQ3QoHqBZUbZhC.jpg',
        duration: '2 mins',
        name: 'Outside the wire'
    },
];
module.exports = allVideos</code></pre>

</div>

<p>We can see from above, each object contains information about the video. Notice the <code>poster</code> attribute which contains the link to a poster image of the video.</p>

<p>Let’s create a <code>videos</code> route since all our request to be made by the frontend is prepended with <code>/videos</code>.</p>

<p>To do this, let’s create a <code>routes</code> folder and add a <code>Video.js</code> file for our <code>/videos</code> route. In this file, we’ll require <code>express</code> and use the express router to create our route.</p>

<pre><code class="language-javascript">const express = require('express')
const router = express.Router()</code></pre>

<p>When we go to the <code>/videos</code> route, we want to get our list of videos, so let’s require the <code>mockData.js</code> file into our <code>Video.js</code> file and make our request.</p>

<pre><code class="language-javascript">const express = require('express')
const router = express.Router()
const videos = require('../mockData')
// get list of videos
router.get('/', (req,res)=>{
    res.json(videos)
})
module.exports = router;</code></pre>

<p>The <code>/videos</code> route is now declared, save the file and it should automatically restart the server. Once it’s started, navigate to <a href="https://localhost:3000/videos">https://localhost:3000/videos</a> and our array is returned in JSON format.</p>

<h2 id="return-data-for-a-single-video">Return Data For A Single Video</h2>

<p>We want to be able to make a request for a particular video in our list of videos. We can fetch a particular video data in our array by using the <code>id</code> we gave it. Let’s make a request, still in our <code>Video.js</code> file.</p>

<pre><code class="language-javascript">
// make request for a particular video
router.get('/:id/data', (req,res)=> {
    const id = parseInt(req.params.id, 10)
    res.json(videos[id])
})</code></pre>

<p>The code above gets the <code>id</code> from the route parameters and converts it to an integer. Then we send the object that matches the <code>id</code> from the <code>videos</code> array back to the client.</p>

<h2 id="streaming-the-videos">Streaming The Videos</h2>

<p>In our <code>app.js</code> file, we created a <code>/video</code> route that serves a video to the client. We want this endpoint to send smaller chunks of the video, instead of serving an entire video file on request.</p>

<p>We want to be able to <em>dynamically</em> serve one of the three videos that are in the <code>allVideos</code> array, and stream the videos in chunks, so:</p>

<p>Delete the <code>/video</code> route from <code>app.js</code>.</p>

<p>We need three videos, so copy the example videos from the tutorial’s <a href="https://github.com/smashingmagazine/Nuxt-Node-video-streaming/tree/main/backend/assets">source code</a> into the <code>assets/</code> directory of your <code>server</code> project. Make sure the filenames for the videos are corresponding to the <code>id</code> in the <code>videos</code> array:</p>

<p>Back in our <code>Video.js</code> file, create the route for streaming videos.</p>

<div class="break-out">

<pre><code class="language-javascript">router.get('/video/:id', (req, res) =&gt; {
    const videoPath = `assets/${req.params.id}.mp4`;
    const videoStat = fs.statSync(videoPath);
    const fileSize = videoStat.size;
    const videoRange = req.headers.range;
    if (videoRange) {
        const parts = videoRange.replace(/bytes=/, "").split("-");
        const start = parseInt(parts[0], 10);
        const end = parts[1]
            ? parseInt(parts[1], 10)
            : fileSize-1;
        const chunksize = (end-start) + 1;
        const file = fs.createReadStream(videoPath, {start, end});
        const head = {
            'Content-Range': `bytes ${start}-${end}/${fileSize}`,
            'Accept-Ranges': 'bytes',
            'Content-Length': chunksize,
            'Content-Type': 'video/mp4',
        };
        res.writeHead(206, head);
        file.pipe(res);
    } else {
        const head = {
            'Content-Length': fileSize,
            'Content-Type': 'video/mp4',
        };
        res.writeHead(200, head);
        fs.createReadStream(videoPath).pipe(res);
    }
});</code></pre>

</div>

<p>If we navigate to <a href="https://localhost:5000/videos/video/outside-the-wire">https://localhost:5000/videos/video/outside-the-wire</a> in our browser, we can see the video streaming.</p>

<h2 id="how-the-streaming-video-route-works">How The Streaming Video Route Works</h2>

<p>There is a fair bit of code written in our stream video route, so let’s look at it line by line.</p>

<pre><code class="language-javascript"> const videoPath = `assets/${req.params.id}.mp4`;
 const videoStat = fs.statSync(videoPath);
 const fileSize = videoStat.size;
 const videoRange = req.headers.range;</code></pre>

<p>First, from our request, we get the <code>id</code> from the route using <code>req.params.id</code> and use it to generate the <code>videoPath</code> to the video. We then read the <code>fileSize</code> using the file system <code>fs</code> we imported. For videos, a user’s browser will send a <code>range</code> parameter in the request. This lets the server know which chunk of the video to send back to the client.</p>

<p>Some browsers send a <em>range</em> in the initial request, but others don’t. For those that don’t, or if for any other reason the browser doesn’t send a range, we handle that in the <code>else</code> block. This code gets the file size and send the first few chunks of the video:</p>

<pre><code class="language-javascript">else {
    const head = {
        'Content-Length': fileSize,
        'Content-Type': 'video/mp4',
    };
    res.writeHead(200, head);
    fs.createReadStream(path).pipe(res);
}</code></pre>

<p>We will handle subsequent requests including the range in an <code>if</code> block.</p>

<div class="break-out">

<pre><code class="language-javascript">if (videoRange) {
        const parts = videoRange.replace(/bytes=/, "").split("-");
        const start = parseInt(parts[0], 10);
        const end = parts[1]
            ? parseInt(parts[1], 10)
            : fileSize-1;
        const chunksize = (end-start) + 1;
        const file = fs.createReadStream(videoPath, {start, end});
        const head = {
            'Content-Range': `bytes ${start}-${end}/${fileSize}`,
            'Accept-Ranges': 'bytes',
            'Content-Length': chunksize,
            'Content-Type': 'video/mp4',
        };
        res.writeHead(206, head);
        file.pipe(res);
    }</code></pre>

</div>

<p>This code above creates a read stream using the <code>start</code> and <code>end</code> values of the range. Set the <code>Content-Length</code> of the response headers to the chunk size that is calculated from the <code>start</code> and <code>end</code> values. We also use <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206">HTTP code 206</a>, signifying that the response contains partial content. This means the browser will keep making requests until it has fetched all chunks of the video.</p>

<h2 id="what-happens-on-unstable-connections">What Happens On Unstable Connections</h2>

<p>If the user is on a slow connection, the network stream will signal it by requesting that the I/O source pauses until the client is ready for more data. This is known as <em>back-pressure</em>. We can take this example one step further and see how easy it is to extend the stream. We can easily add compression, too!</p>

<div class="break-out">

<pre><code class="language-javascript">const start = parseInt(parts[0], 10);
        const end = parts[1]
            ? parseInt(parts[1], 10)
            : fileSize-1;
        const chunksize = (end-start) + 1;
        const file = fs.createReadStream(videoPath, {start, end});</code></pre>

</div>

<p>We can see above that a <code>ReadStream</code> is created and serves the video chunk by chunk.</p>

<div class="break-out">

<pre><code class="language-javascript">const head = {
            'Content-Range': `bytes ${start}-${end}/${fileSize}`,
            'Accept-Ranges': 'bytes',
            'Content-Length': chunksize,
            'Content-Type': 'video/mp4',
        };
res.writeHead(206, head);
        file.pipe(res);</code></pre>

</div>

<p>The request header contains the <code>Content-Range</code>, which is the start and end changing to get the next chunk of video to stream to the frontend, the <code>content-length</code> is the chunk of video sent. We also specify the type of content we are streaming which is <code>mp4</code>. The writehead of 206 is set to respond with only newly made streams.</p>

<h2 id="creating-a-caption-file-for-our-videos">Creating A Caption File For Our Videos</h2>

<p>This is what a <code>.vtt</code> caption file looks like.</p>

<pre><code class="language-javascript">WEBVTT

00:00:00.200 --> 00:00:01.000
Creating a tutorial can be very

00:00:01.500 --> 00:00:04.300
fun to do.</code></pre>

<p>Captions files contain text for what is said in a video. It also contains time codes for when each line of text should be displayed. We want our videos to have captions, and we won’t be creating our own caption file for this tutorial, so you can head over to the captions folder in the <code>assets</code> directory in <a href="https://github.com/Dunebook/Nuxt-Node-video-streaming/tree/main/backend/assets/captions">the repo</a> and download the captions.</p>

<p>Let’s create a new route that will handle the caption request:</p>

<div class="break-out">

<pre><code class="language-javascript">router.get('/video/:id/caption', (req, res) => res.sendFile(`assets/captions/${req.params.id}.vtt`, { root: __dirname }));</code></pre>

</div>

<div class="partners__lead-place"></div>

<h2 id="building-our-frontend">Building Our Frontend</h2>

<p>To get started on the visual part of our system, we would have to build out our frontend scaffold.</p>

<p><strong>Note</strong>: <em>You need vue-cli to create our app. If you don’t have it installed on your computer, you can run <code>npm install -g @vue/cli</code> to install it.</em></p>

<h2 id="installation">Installation</h2>

<p>At the root of our project, let&rsquo;s create our front-end folder:</p>

<pre><code class="language-bash">mkdir frontend
cd frontend</code></pre>

<p>and in it, we initialize our <code>package.json</code> file, copy and paste the following in it:</p>

<pre><code class="language-javascript">{
  "name": "my-app",
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "generate": "nuxt generate",
    "start": "nuxt start"
  }
}</code></pre>

<p>then install <code>nuxt</code>:</p>

<pre><code class="language-bash">npm add nuxt</code></pre>

<p>and execute the following command to run Nuxt.js app:</p>

<pre><code class="language-bash">npm run dev</code></pre>

<h2 id="our-nuxt-file-structure">Our Nuxt File Structure</h2>

<p>Now that we have Nuxt installed, we can begin laying out our frontend.</p>

<p>First, we need to create a <code>layouts</code> folder at the root of our app. This folder defines the layout of the app, no matter the page we navigate to. Things like our navigation bar and footer are found here. In the frontend folder, we create <code>default.vue</code> for our default layout when we start our frontend app.</p>

<pre><code class="language-javascript">mkdir layouts
cd layouts
touch default.vue</code></pre>

<p>Then a <code>components</code> folder to create all our components. We will be needing only two components, <code>NavBar</code> and <code>video</code> component. So in our root folder of frontend we:</p>

<pre><code class="language-javascript">mkdir components
cd components
touch NavBar.vue
touch Video.vue</code></pre>

<p>Finally, a pages folder where all our pages like <code>home</code> and <code>about</code> can be created. The two pages we need in this app, are the <code>home</code> page displaying all our videos and video information and a dynamic player page that routes to the video we click on.</p>

<pre><code class="language-javascript">mkdir pages
cd pages
touch index.vue
mkdir player
cd player
touch _name.vue</code></pre>

<p>Our frontend directory now looks like this:</p>

<pre><code class="language-javascript">|-frontend
  |-components
    |-NavBar.vue
    |-Video.vue
  |-layouts
    |-default.vue
  |-pages
    |-index.vue
    |-player
      |-_name.vue
  |-package.json
  |-yarn.lock</code></pre> 

<h2 id="navbar-component">Navbar Component</h2>

<p>Our <code>NavBar.vue</code> looks like this:</p>

<pre><code class="language-javascript">&lt;template&gt;
    &lt;div class="navbar"&gt;
        &lt;h1&gt;Streaming App&lt;/h1&gt;
    &lt;/div&gt;
&lt;/template&gt;
&lt;style scoped&gt;
.navbar {
    display: flex;
    background-color: #161616;
    justify-content: center;
    align-items: center;
}
h1{
    color:#a33327;
}
&lt;/style&gt;</code></pre>

<p>The <code>NavBar</code> has a <code>h1</code> tag that displays <strong>Streaming App</strong>, with some little styling.</p>

<p>Let’s import the <code>NavBar</code> into our <code>default.vue</code> layout.</p>

<pre><code class="language-javascript">// default.vue
&lt;template&gt;
 &lt;div&gt;
   &lt;NavBar /&gt;
   &lt;nuxt /&gt;
 &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
import NavBar from "@/components/NavBar.vue"
export default {
    components: {
        NavBar,
    }
}
&lt;/script&gt;</code></pre>

<p>The <code>default.vue</code> layout now contains our <code>NavBar</code> component and the <code>&lt;nuxt /&gt;</code> tag after it signifies where any page we create will be displayed.</p>

<p>In our <code>index.vue</code> (which is our homepage), let’s make a request to <code>https://localhost:5000/videos</code> to get all the videos from our server. Passing the data as a prop to our <code>video.vue</code> component we will create later. But for now, we have already imported it.</p>

<pre><code class="language-javascript">&lt;template&gt;
&lt;div&gt;
  &lt;Video :videoList="videos"/&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
import Video from "@/components/Video.vue"
export default {
  components: {
    Video
  },
head: {
    title: "Home"
  },
    data() {
      return {
        videos: []
      }
    },
    async fetch() {
      this.videos = await fetch(
        'https://localhost:5000/videos'
      ).then(res =&gt; res.json())
    }
}
&lt;/script&gt;</code></pre>

<h2 id="video-component">Video Component</h2>

<p>Below, we first declare our prop. Since the video data is now available in the component, using Vue’s <code>v-for</code> we iterate on all the data received and for each one, we display the information. We can use the <code>v-for</code> directive to loop through the data and display it as a list. Some basic styling has also been added.</p>

<pre><code class="language-javascript">&lt;template&gt;
&lt;div&gt;
  &lt;div class="container"&gt;
    &lt;div
    v-for="(video, id) in videoList"
    :key="id"
    class="vid-con"
  &gt;
    &lt;NuxtLink :to="`/player/${video.id}`"&gt;
    &lt;div
      :style="{
        backgroundImage: `url(${video.poster})`
      }"
      class="vid"
    &gt;&lt;/div&gt;
    &lt;div class="movie-info"&gt;
      &lt;div class="details"&gt;
      &lt;h2&gt;{{video.name}}&lt;/h2&gt;
      &lt;p&gt;{{video.duration}}&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/NuxtLink&gt;
  &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
export default {
    props:['videoList'],
}
&lt;/script&gt;
&lt;style scoped&gt;
.container {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: 2rem;
}
.vid-con {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  justify-content: center;
  width: 50%;
  max-width: 16rem;
  margin: auto 2em;
  
}
.vid {
  height: 15rem;
  width: 100%;
  background-position: center;
  background-size: cover;
}
.movie-info {
  background: black;
  color: white;
  width: 100%;
}
.details {
  padding: 16px 20px;
}
&lt;/style&gt;</code></pre>

<p>We also notice that the <code>NuxtLink</code> has a dynamic route, that is routing to the <code>/player/video.id</code>.</p>

<p>The functionality we want is when a user clicks on any of the videos, it starts streaming. To achieve this, we make use of the dynamic nature of the <code>_name.vue</code> route.</p>

<p>In it, we create a video player and set the source to our endpoint for streaming the video, but we dynamically append which video to play to our endpoint with the help of  <code>this.$route.params.name</code> that captures which parameter the link received.</p>

<div class="break-out">

<pre><code class="language-javascript">&lt;template&gt;
    &lt;div class="player"&gt;
        &lt;video controls muted autoPlay&gt;
            &lt;source :src="`https://localhost:5000/videos/video/${vidName}`" type="video/mp4"&gt;
        &lt;/video&gt;
    &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
export default {
 data() {
      return {
        vidName: ''
      }
    },
mounted(){
    this.vidName = this.$route.params.name
}
}
&lt;/script&gt;
&lt;style scoped&gt;
.player {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 2em;
}
&lt;/style&gt;</code></pre>

</div>

<p>When we click on any of the video we get:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/d2b6e82b-9aa5-4c25-8f03-56c8bac69b74/01-nuxtjs-video-preview.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="397"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/d2b6e82b-9aa5-4c25-8f03-56c8bac69b74/01-nuxtjs-video-preview.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/d2b6e82b-9aa5-4c25-8f03-56c8bac69b74/01-nuxtjs-video-preview.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/d2b6e82b-9aa5-4c25-8f03-56c8bac69b74/01-nuxtjs-video-preview.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/d2b6e82b-9aa5-4c25-8f03-56c8bac69b74/01-nuxtjs-video-preview.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/d2b6e82b-9aa5-4c25-8f03-56c8bac69b74/01-nuxtjs-video-preview.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/d2b6e82b-9aa5-4c25-8f03-56c8bac69b74/01-nuxtjs-video-preview.jpg"
			
			sizes="100vw"
			alt="Nuxt video streaming app final result"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Video streaming gets started when user clicks the thumbnail. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/234cb7d7-81ac-469a-ba08-a792edbfe159/1-building-video-streaming-app-nuxt-js-node-express.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="adding-our-caption-file">Adding Our Caption File</h2>

<p>To add our track file, we make sure all the <code>.vtt</code> files in the <em>captions</em> folder have the same name as our <code>id</code>. Update our video element with the track, making a request for the captions.</p>

<div class="break-out">

<pre><code class="language-javascript">&lt;template&gt;
    &lt;div class="player"&gt;
        &lt;video controls muted autoPlay crossOrigin="anonymous"&gt;
            &lt;source :src="`https://localhost:5000/videos/video/${vidName}`" type="video/mp4"&gt;
            &lt;track label="English" kind="captions" srcLang="en" :src="`https://localhost:5000/videos/video/${vidName}/caption`" default&gt;
        &lt;/video&gt;
    &lt;/div&gt;
&lt;/template&gt;
</code></pre>

</div>

<p>We’ve added <code>crossOrigin=&quot;anonymous&quot;</code> to the video element; otherwise, the request for captions will fail. Now refresh and you’ll see captions have been added successfully.</p>

<h2 id="what-to-keep-in-mind-when-building-resilient-video-streaming">What To Keep In Mind When Building Resilient Video Streaming.</h2>

<p>When building streaming applications like Twitch, Hulu or Netflix, there are a number of things that are put into consideration:</p>

<ul>
<li><strong>Video data processing pipeline</strong><br />
This can be a technical challenge as high-performing servers are needed to serve millions of videos to users. High latency or downtime should be avoided at all costs.</li>
<li><strong>Caching</strong><br />
Caching mechanisms should be used when building this type of application example Cassandra, Amazon S3, AWS SimpleDB.</li>
<li><strong>Users’ geography</strong><br />
Considering the geography of your users should be thought about for distribution.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>In this tutorial, we have seen how to create a server in Node.js that streams videos, generates captions for those videos, and serves metadata of the videos. We’ve also seen how to use Nuxt.js on the frontend to consume the endpoints and the data generated by the server.</p>

<p>Unlike other frameworks, building an application with Nuxt.js and Express.js is quite easy and fast. The cool part about Nuxt.js is the way it manages your routes and makes you structure your apps better.</p>

<ul>
<li>You can get more information about Nuxt.js <a href="https://nuxtjs.org/guide">here</a>.</li>
<li>You can get the <a href="https://github.com/smashingmagazine/Nuxt-Node-video-streaming/">source code on Github</a>.</li>
</ul>

<h3 id="resources">Resources</h3>

<ul>
<li>“<a href="https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_delivery/Adding_captions_and_subtitles_to_HTML5_video">Adding Captions And Subtitles To HTML5 Video</a>,” MDN Web Docs</li>
<li>“<a href="https://web.archive.org/web/20160117160743/https://screenfont.ca/learn/">Understanding Captions And Subtitles</a>,” Screenfont.ca</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(ks, vf, yk, il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Timi Omoyeni</author><title>Reactivity In Vue</title><link>https://www.smashingmagazine.com/2021/03/reactivity-in-vue/</link><pubDate>Thu, 25 Mar 2021 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2021/03/reactivity-in-vue/</guid><description>Reactivity is the ability for a variable (array, string, number, object, etc) to update when its value or any other variable that it makes reference to is changed after declaration. In this article, Timi Omoyeni is going to look at reactivity in Vue, how it works, and how you can create reactive variables using newly created methods and functions.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2021/03/reactivity-in-vue/" />
              <title>Reactivity In Vue</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Reactivity In Vue</h1>
                  
                    
                    <address>Timi Omoyeni</address>
                  
                  <time datetime="2021-03-25T10:00:00&#43;00:00" class="op-published">2021-03-25T10:00:00+00:00</time>
                  <time datetime="2021-03-25T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>In this article, we’re going to look at reactivity in Vue, how it works, and how we can create reactive variables using newly created methods and functions. This article is targeted at developers who have a good understanding of how Vue 2.x works and are looking to get familiar with the new <a href="https://v3.vuejs.org/">Vue 3</a>.</p>

<p>We’re going to build a simple application to better understand this topic. The code for this app can be found <a href="https://github.com/Timibadass/vue-reactivity">on GitHub</a>.</p>

<p>By default, <strong>JavaScript isn’t reactive</strong>. This means that if we create the variable <code>boy</code> and reference it in part A of our application, then proceed to modify <code>boy</code> in part B, part A will not update with the new value of <code>boy</code>.</p>

<pre><code class="language-javascript">let framework = 'Vue';
let sentence = `${framework} is awesome`;
console.log(sentence)
 // logs "Vue is awesome"
framework = 'React';
console.log(sentence)
//should log "React is awesome" if 'sentence' is reactive.</code></pre>

<p>The snippet above is a perfect example of the non-reactive nature of JavaScript — hence, why the change isn’t reflected in the <code>sentence</code> variable.</p>

<p>In Vue 2.x, <code>props</code>, <code>computed</code>, and <code>data()</code> were all reactive by default, with the exception of properties that are not present in <code>data</code> when such components are created. This means that when a component is injected into the <a href="https://en.wikipedia.org/wiki/Document_Object_Model">DOM</a>, only the <a href="https://vuejs.org/v2/guide/instance.html#Data-and-Methods">existing properties in the component</a>’s <a href="https://vuejs.org/v2/guide/instance.html#Data-and-Methods"><code>data</code></a> object would cause the component to update if and when such properties change.</p>

<p>Internally, Vue 3 uses the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy"><code>Proxy</code> object</a> (an ECMAScript 6 feature) to ensure that these properties are reactive, but it still provides the option to use <code>Object.defineProperty</code> from <a href="https://vuejs.org/v2/guide/reactivity.html#How-Changes-Are-Tracked">Vue 2</a> for Internet Explorer support (ECMAScript 5). This method defines a new property directly on an object, or modifies an existing property on an object, and returns the object.</p>

<p>At first glance and since most of us already know that reactivity is not new in Vue, it might seem unnecessary to make use of these properties, but the Options API has its limitations when you’re dealing with a large application with reusable functions in several parts of the application. To this end, the new <a href="https://v3.vuejs.org/guide/composition-api-introduction.html">Composition API</a> was introduced to help with abstracting logic in order to make a code base easier to read and maintain. Also, we can now easily make any variable reactive regardless of its <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures">data type</a> using any of the new properties and methods.</p>

<p>When we use the <code>setup</code> option, which serves as the entry point for the Composition API, the <code>data</code> object, <code>computed</code> properties, and <code>methods</code> are inaccessible because the component instance has not yet been created when <code>setup</code> is executed. This makes it impossible to take advantage of the <strong>built-in reactivity</strong> in any of these features in <code>setup</code>. In this tutorial, we’re going to learn about all of the ways we can do this.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p><p>Meet <a data-instant href="/the-smashing-newsletter/"><strong>Smashing Email Newsletter</strong></a> with useful tips on front-end, design &amp; UX. Subscribe and <strong>get “Smart Interface Design Checklists”</strong> &mdash; a <strong>free PDF deck</strong> with 150+ questions to ask yourself when designing and building almost <em>anything</em>.</p><div><section class="nlbf"><form action="//smashingmagazine.us1.list-manage.com/subscribe/post?u=16b832d9ad4b28edf261f34df&amp;id=a1666656e0" method="post"><div class="nlbwrapper"><label for="mce-EMAIL-hp" class="sr-only">Your (smashing) email</label><div class="nlbgroup"><input type="email" name="EMAIL" class="nlbf-email" id="mce-EMAIL-hp" placeholder="Your email">
<input type="submit" value="Meow!" name="subscribe" class="nlbf-button"></div></div></form><style>.c-garfield-the-cat .nlbwrapper{margin-bottom: 0;}.nlbf{display:flex;padding-bottom:.25em;padding-top:.5em;text-align:center;letter-spacing:-.5px;color:#fff;font-size:1.15em}.nlbgroup:hover{box-shadow:0 1px 7px -5px rgba(50,50,93,.25),0 3px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025)}.nlbf .nlbf-button,.nlbf .nlbf-email{flex-grow:1;flex-shrink:0;width:auto;margin:0;padding:.75em 1em;border:0;border-radius:11px;background:#fff;font-size:1em;box-shadow:none}.promo-box .nlbf-button:focus,.promo-box input.nlbf-email:active,.promo-box input.nlbf-email:focus{box-shadow:none}.nlbf-button:-ms-input-placeholder,.nlbf-email:-ms-input-placeholder{color:#777;font-style:italic}.nlbf-button::-webkit-input-placeholder,.nlbf-email::-webkit-input-placeholder{color:#777;font-style:italic}.nlbf-button:-ms-input-placeholder,.nlbf-button::-moz-placeholder,.nlbf-button::placeholder,.nlbf-email:-ms-input-placeholder,.nlbf-email::-moz-placeholder,.nlbf-email::placeholder{color:#777;font-style:italic}.nlbf .nlbf-button{transition:all .2s ease-in-out;color:#fff;background-color:#0168b8;font-weight:700;box-shadow:0 1px 1px rgba(0,0,0,.3);width:100%;border:0;border-left:1px solid #ddd;flex:2;border-top-left-radius:0;border-bottom-left-radius:0}.nlbf .nlbf-email{border-top-right-radius:0;border-bottom-right-radius:0;width:100%;flex:4;min-width:150px}@media all and (max-width:650px){.nlbf .nlbgroup{flex-wrap:wrap;box-shadow:none}.nlbf .nlbf-button,.nlbf .nlbf-email{border-radius:11px;border-left:none}.nlbf .nlbf-email{box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);min-width:100%}.nlbf .nlbf-button{margin-top:1em;box-shadow:0 1px 1px rgba(0,0,0,.5)}}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus,.nlbf .nlbf-button:hover{cursor:pointer;color:#fff;background-color:#0168b8;border-color:#dadada;box-shadow:0 1px 1px rgba(0,0,0,.3)}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus{outline:0!important;text-shadow:1px 1px 1px rgba(0,0,0,.3);box-shadow:inset 0 3px 3px rgba(0,0,0,.3)}.nlbgroup{display:flex;box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);border-radius:11px;transition:box-shadow .2s ease-in-out}.nlbwrapper{display:flex;flex-direction:column;justify-content:center}.nlbf form{width:100%}.nlbf .nlbgroup{margin:0}.nlbcaption{font-size:.9em;line-height:1.5em;color:#fff;border-radius:11px;padding:.5em 1em;display:inline-block;background-color:#0067b859;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.wf-loaded-stage2 .nlbf .nlbf-button{font-family:Mija}.mts{margin-top: 5px !important;}.mbn{margin-bottom: 0 !important;}</style></section><p class="mts mbn"><small class="promo-box__footer mtm block grey"><em>Once a week. Useful tips on <a href="https://www.smashingmagazine.com/the-smashing-newsletter/">front-end &amp; UX</a>. Trusted by 207.000 friendly folks.</em></small></p></div></p>
</div>
</div>
<div class="feature-panel-right-col">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-firechat.svg"
    alt="Feature Panel"
    width="310"
    height="400"
/>

</div>

<p></div>
</aside>
</div></p>

<h2 id="the-reactive-method">The Reactive Method</h2>

<p>According to the documentation, the <code>reactive</code> method, which is the equivalent of <code>Vue.observable()</code> in Vue 2.6, can be useful when we’re trying to create an object all of whose properties are reactive (such as the <code>data</code> object in the Options API). Under the hood, the <code>data</code> object in the Options API uses this method to make all of the properties in it reactive.</p>

<p>But we can create our own reactive object like this:</p>

<div class="break-out">

<pre><code class="language-javascript">import { reactive } from 'vue'

// reactive state
let user = reactive({
        "id": 1,
        "name": "Leanne Graham",
        "username": "Bret",
        "email": "Sincere@april.biz",
        "address": {
            "street": "Kulas Light",
            "suite": "Apt. 556",
            "city": "Gwenborough",
            "zipcode": "92998-3874",
            "geo": {
                "lat": "-37.3159",
                "lng": "81.1496"
            }
        },
        "phone": "1-770-736-8031 x56442",
        "website": "hildegard.org",
        "company": {
            "name": "Romaguera-Crona",
            "catchPhrase": "Multi-layered client-server neural-net",
            "bs": "harness real-time e-markets"
        },
        "cars": {
            "number": 0
        }
    })</code></pre>

</div>

<p>Here, we imported the <code>reactive</code> method from Vue, and then we declared our <code>user</code> variable by passing its value to this function as an argument. In doing so, we’ve made <code>user</code> reactive, and, thus, if we use <code>user</code> in our template and if either the object or a property of this object should change, then this value will get automatically updated in this template.</p>

<h2 id="ref"><code>ref</code></h2>

<p>Just as we have a method for making objects reactive, we also need one to make <strong>other standalone primitive values</strong> (strings, booleans, undefined values, numbers, etc.) and arrays reactive. During development, we would work with these other data types while also needing them to be reactive. The first approach we might think of would be to use <code>reactive</code> and pass in the value of the variable that we want to make reactive.</p>

<pre><code class="language-javascript">import { reactive } from 'vue'

const state = reactive({
  users: [],
});</code></pre>

<p>Because <code>reactive</code> has deep reactive conversion, <code>user</code> as a property would also be reactive, thereby achieving our goal; hence, <code>user</code> would always update anywhere it is used in the template of such an app. But with the <code>ref</code> property, we can make any variable with any data type reactive by passing the value of that variable to <code>ref</code>. This method also works for objects, but it nests the object one level deeper than when the <code>reactive</code> method is used.</p>

<pre><code class="language-javascript">let property = {
  rooms: '4 rooms',
  garage: true,
  swimmingPool: false
}
let reactiveProperty = ref(property)
console.log(reactiveProperty)
// prints {
// value: {rooms: "4 rooms", garage: true, swimmingPool: false}
// }</code></pre>

<p>Under the hood, <code>ref</code> takes this argument passed to it and converts it into an object with a key of <code>value</code>. This means, we can access our variable by calling <code>variable.value</code>, and we can also modify its value by calling it in the same way.</p>

<pre><code class="language-javascript">import {ref} from 'vue'
let age = ref(1)

console.log(age.value)
//prints 1
age.value++
console.log(age.value)
//prints 2</code></pre>

<p>With this, we can import <code>ref</code> into our component and create a reactive variable:</p>

<pre><code class="language-javascript">&lt;template&gt;
  &lt;div class="home"&gt;
    &lt;form @click.prevent=""&gt;
      &lt;table&gt;
        &lt;tr&gt;
          &lt;th&gt;Name&lt;/th&gt;
          &lt;th&gt;Username&lt;/th&gt;
          &lt;th&gt;email&lt;/th&gt;
          &lt;th&gt;Edit Cars&lt;/th&gt;
          &lt;th&gt;Cars&lt;/th&gt;
        &lt;/tr&gt;
        &lt;tr v-for="user in users" :key="user.id"&gt;
          &lt;td&gt;{{ user.name }}&lt;/td&gt;
          &lt;td&gt;{{ user.username }}&lt;/td&gt;
          &lt;td&gt;{{ user.email }}&lt;/td&gt;
          &lt;td&gt;
            &lt;input
              type="number"
              style="width: 20px;"
              name="cars"
              id="cars"
              v-model.number="user.cars.number"
            /&gt;
          &lt;/td&gt;
          &lt;td&gt;
            &lt;cars-number :cars="user.cars" /&gt;
          &lt;/td&gt;
        &lt;/tr&gt;
      &lt;/table&gt;
      &lt;p&gt;Total number of cars: {{ getTotalCars }}&lt;/p&gt;
    &lt;/form&gt;
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
  // @ is an alias to /src
  import carsNumber from "@/components/cars-number.vue";
  import axios from "axios";
  import { ref } from "vue";
  export default {
    name: "Home",
    data() {
      return {};
    },
    setup() {
      let users = ref([]);
      const getUsers = async () =&gt; {
        let { data } = await axios({
          url: "data.json",
        });
        users.value = data;
      };
      return {
        users,
        getUsers,
      };
    },
    components: {
      carsNumber,
    },
    created() {
      this.getUsers();
    },
    computed: {
      getTotalCars() {
        let users = this.users;
        let totalCars = users.reduce(function(sum, elem) {
          return sum + elem.cars.number;
        }, 0);
        return totalCars;
    },
  };
&lt;/script&gt;</code></pre>

<p>Here, we imported <code>ref</code> in order to create a reactive <code>users</code> variable in our component. We then imported <code>axios</code> to fetch data from a JSON file in the <code>public</code> folder, and we imported our <code>carsNumber</code> component, which we’ll be creating later on. The next thing we did was create a reactive <code>users</code> variable using the <code>ref</code> method, so that <code>users</code> can update whenever the response from our JSON file changes.</p>

<p>We also created a <code>getUser</code> function that fetches the <code>users</code> array from our JSON file using <a href="https://github.com/axios/axios">axios</a>, and we assigned the value from this request to the <code>users</code> variable. Finally, we created a computed property that computes the total number of cars that our users have as we have modified it in the template section.</p>

<p>It is important to note that when accessing <code>ref</code> properties that are returned in the template section or outside of <code>setup()</code>, they are <a href="https://v3.vuejs.org/guide/reactivity-fundamentals.html#ref-unwrapping">automatically shallow unwrapped</a>. This means that <code>refs</code> that are an object would still require a <code>.value</code> in order to be accessed. Because <code>users</code> is an array, we could simply use <code>users</code> and not <code>users.value</code> in <code>getTotalCars</code>.</p>

<p>In the template section, we displayed a table that displays each user’s information, together with a <code>&lt;cars-number /&gt;</code> component. This component accepts a <code>cars</code> prop that is displayed in each user’s row as the number of cars they have. This value updates whenever the value of <code>cars</code> <strong>changes in the user object</strong>, which is exactly how the <code>data</code> object or <code>computed</code> property would work if we were working with the Options API.</p>

<h2 id="torefs"><code>toRefs</code></h2>

<p>When we use the Composition API, the <code>setup</code> function accepts two arguments: <code>props</code> and <code>context</code>. This <code>props</code> is passed from the component to <code>setup()</code>, and it makes it possible to access the props that the component has from inside this new API. This method is particularly useful because it allows for the <strong>destructuring of objects</strong> without losing its reactivity.</p>

<pre><code class="language-javascript">&lt;template&gt;
  &lt;p&gt;{{ cars.number }}&lt;/p&gt;
&lt;/template&gt;
&lt;script&gt;
  export default {
    props: {
      cars: {
        type: Object,
        required: true,
      },
      gender: {
        type: String,
        required: true,
      },
    },
    setup(props) {
      console.log(props);
   // prints {gender: "female", cars: Proxy}
    },
  };
&lt;/script&gt;
&lt;style&gt;&lt;/style&gt;</code></pre>

<p>To use a value that is an object from <code>props</code> in the Composition API while ensuring it maintains its reactivity, we make use of <code>toRefs</code>. This method takes a reactive object and converts it into a plain object in which each property of the original reactive object becomes a <code>ref</code>. What this means is that the <code>cars</code> prop…</p>

<pre><code class="language-javascript">cars: {
  number: 0
}</code></pre>

<p>… would now become this:</p>

<pre><code class="language-javascript">{
  value: cars: {
    number: 0
  }</code></pre>

<p>With this, we can make use of <code>cars</code> inside any part of the setup API while still maintaining its reactivity.</p>

<pre><code class="language-javascript"> setup(props) {
      let { cars } = toRefs(props);
      console.log(cars.value);
      // prints {number: 0}
    },</code></pre>

<p>We can watch this new variable using the Composition API’s <code>watch</code> and react to this change however we might want to.</p>

<pre><code class="language-javascript">setup(props) {
      let { cars } = toRefs(props);
      watch(
        () =&gt; cars,
        (cars, prevCars) =&gt; {
          console.log("deep ", cars.value, prevCars.value);
        },
        { deep: true }
      );
    }</code></pre>

<div class="partners__lead-place"></div>

<h2 id="toref"><code>toRef</code></h2>

<p>Another common use case we could be faced with is <strong>passing a value</strong> that is not necessarily an object but rather one of the data types that work with <code>ref</code> (array, number, string, boolean, etc.). With <code>toRef</code>, we can create a reactive property (i.e. <code>ref</code>) from a source reactive object. Doing this would ensure that the property remains reactive and would update whenever the parent source changes.</p>

<pre><code class="language-javascript">const cars = reactive({
  Toyota: 1,
  Honda: 0
})

const NumberOfHondas = toRef(state, 'Honda')

NumberOfHondas.value++
console.log(state.Honda) // 1

state.Honda++
console.log(NumberOfHondas.value) // 2</code></pre>

<p>Here, we created a reactive object using the <code>reactive</code> method, with the properties <code>Toyota</code> and <code>Honda</code>. We also made use of <code>toRef</code> to create a reactive variable out of <code>Honda</code>. From the example above, we can see that when we update <code>Honda</code> using either the reactive <code>cars</code> object or <code>NumberOfHondas</code>, the value gets updated in both instances.</p>

<p>This method is similar and yet so different from the <code>toRefs</code> method that we covered above in the sense that it maintains its connection to its source and can be used for strings, arrays, and numbers. Unlike with <code>toRefs</code>, we do not need to worry about the existence of the property in its source at the time of creation, because if this property does not exist at the time that this <code>ref</code> is created and instead returns <code>null</code>, it would still be stored as a valid property, with a form of <a href="https://v3.vuejs.org/guide/reactivity-computed-watchers.html#watch"><code>watcher</code></a> put in place, so that when this value changes, this <code>ref</code> created using <code>toRef</code> would also be updated.</p>

<p>We can also use this method to create a <strong>reactive property</strong> from <code>props</code>. That would look like this:</p>

<pre><code class="language-javascript">&lt;template&gt;
  &lt;p&gt;{{ cars.number }}&lt;/p&gt;
&lt;/template&gt;
&lt;script&gt;
  import { watch, toRefs, toRef } from "vue";
  export default {
    props: {
      cars: {
        type: Object,
        required: true,
      },
      gender: {
        type: String,
        required: true,
      },
    },
    setup(props) {
      let { cars } = toRefs(props);
      let gender = toRef(props, "gender");
      console.log(gender.value);
      watch(
        () =&gt; cars,
        (cars, prevCars) =&gt; {
          console.log("deep ", cars.value, prevCars.value);
        },
        { deep: true }
      );
    },
  };
&lt;/script&gt;</code></pre>

<p>Here, we created a <code>ref</code> that would be based on the <code>gender</code> property gotten from <code>props</code>. This comes in handy when we want to perform extra operations on the prop of a particular component.</p>

<h2 id="conclusion">Conclusion</h2>

<p>In this article, we have looked at how reactivity in Vue works using some of the newly introduced methods and functions from Vue 3. We started by looking at what reactivity is and how Vue makes use of the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy"><code>Proxy</code> object</a> behind the scenes to achieve this. We also looked at how we can create reactive objects using <code>reactive</code> and how to create reactive properties using <code>ref</code>.</p>

<p>Finally, we looked at how to convert reactive objects to plain objects, each of whose properties are a <a href="https://v3.vuejs.org/api/refs-api.html#ref"><code>ref</code></a> pointing to the corresponding property of the original object, and we saw how to create a <a href="https://v3.vuejs.org/api/refs-api.html#ref"><code>ref</code></a> for a property on a reactive source object.</p>

<h3 id="other-resources">Other Resources</h3>

<ul>
<li>“<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy">Proxy</a>” (object), MDN Web Docs</li>
<li>“<a href="https://v3.vuejs.org/guide/reactivity-fundamentals.html#ref-unwrapping">Reactivity Fundamentals</a>”, Vue.js</li>
<li>“<a href="https://v3.vuejs.org/api/refs-api.html#refs">Refs</a>”, Vue.js</li>
<li>“<a href="https://v3.vuejs.org/guide/composition-api-introduction.html#lifecycle-hook-registration-inside-setup">Lifecycle Hook Registration Inside <code>setup</code></a>”, Vue.js</li>
</ul>

<div class="partners__lead-place"></div>

<h3 id="further-reading">Further Reading</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2023/03/building-complex-forms-vue/">Building Complex Forms In Vue</a></li>
<li><a href="https://www.smashingmagazine.com/2023/05/big-tooling-upgrades-large-organizations/">How To Deal With Big Tooling Upgrades In Large Organizations</a></li>
<li><a href="https://www.smashingmagazine.com/2024/10/build-static-rss-reader-fight-fomo/">Build A Static RSS Reader To Fight Your Inner FOMO</a></li>
<li><a href="https://www.smashingmagazine.com/2024/05/netlify-platform-primitives/">The Era Of Platform Primitives Is Finally Here</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(ks, vf, yk, il, al, mrn)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Joseph Zimmerman</author><title>What’s Coming To VueX?</title><link>https://www.smashingmagazine.com/2020/12/vuex-library/</link><pubDate>Thu, 24 Dec 2020 09:30:13 +0000</pubDate><guid>https://www.smashingmagazine.com/2020/12/vuex-library/</guid><description>Vuex is the go-to state management library for Vue applications, and the Vue core team has some big plans to make it better than ever. Vuex 5 will work more like a composition API alternative but keeps all the benefits of using an official state management library. In this article, Joseph Zimmerman will take a look at what will be changing.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2020/12/vuex-library/" />
              <title>What’s Coming To VueX?</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>What’s Coming To VueX?</h1>
                  
                    
                    <address>Joseph Zimmerman</address>
                  
                  <time datetime="2020-12-24T09:30:13&#43;00:00" class="op-published">2020-12-24T09:30:13+00:00</time>
                  <time datetime="2020-12-24T09:30:13&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p><a href="https://vuex.vuejs.org/">Vuex</a> is <em>the</em> solution for state management in Vue applications. The next version — Vuex 4 — is making its way through the final steps before officially releasing. This release will bring full compatibility with Vue 3, but doesn’t add new features. While Vuex has always been a powerful solution, and the first choice for many developers for state management in Vue, some developers had hoped to see more workflow issues addressed. However, even as Vuex 4 is just getting out the door, <a href="https://github.com/kiaking">Kia King Ishii</a> (a Vue core team member) is <a href="https://youtu.be/dQ5btkDrdJ8">talking about his plans for Vuex 5</a>, and I’m so excited by what I saw that I had to share it with you all. Note that Vuex 5 plans are <em>not</em> finalized, so some things may change before Vuex 5 is released, but if it ends up mostly similar to what you see in this article, it should be a big improvement to the developer experience.</p>

<p>With the advent of Vue 3 and it’s <a href="https://v3.vuejs.org/guide/composition-api-introduction.html#why-composition-api">composition API</a>, people have been looking into hand-built simple alternatives. For example, <em><a href="https://dev.to/blacksonic/you-might-not-need-vuex-with-vue-3-52e4">You Might Not Need Vuex</a></em> demonstrates a relatively simple, yet flexible and robust pattern for using the composition API along with <code>provide/inject</code> to create shared state stores. As Gábor states in his article, though, this (and other alternatives) should only be used in smaller applications because they lack all those things that aren’t directly about the code: community support, documentation, conventions, good <a href="https://nuxtjs.org/">Nuxt</a> integrations, and developer tools.</p>

<p>That last one has always been one of the biggest issues for me. The <a href="https://github.com/vuejs/vue-devtools">Vue devtools</a> browser extension has always been an amazing tool for debugging and developing Vue apps, and losing the Vuex inspector with “time travel” would be a pretty big loss for debugging any non-trivial applications.</p>














<figure class="
  
  
  ">
  
    <a href="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/efd616f6-9b32-4a34-8de3-77cb86394e72/vuex-devtools.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			
			
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/efd616f6-9b32-4a34-8de3-77cb86394e72/vuex-devtools.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/efd616f6-9b32-4a34-8de3-77cb86394e72/vuex-devtools.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/efd616f6-9b32-4a34-8de3-77cb86394e72/vuex-devtools.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/efd616f6-9b32-4a34-8de3-77cb86394e72/vuex-devtools.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/efd616f6-9b32-4a34-8de3-77cb86394e72/vuex-devtools.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/efd616f6-9b32-4a34-8de3-77cb86394e72/vuex-devtools.png"
			
			sizes="100vw"
			alt="Debugging Vuex with Vue Devtools"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Debugging Vuex with the Vue Devtools. (<a href='https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/efd616f6-9b32-4a34-8de3-77cb86394e72/vuex-devtools.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Thankfully, with Vuex 5 we’ll be able to have our cake and eat it too. It will work more like these composition API alternatives but keep all the benefits of using an official state management library. Now let’s take a look at what will be changing.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p><p>Meet <a data-instant href="/the-smashing-newsletter/"><strong>Smashing Email Newsletter</strong></a> with useful tips on front-end, design &amp; UX. Subscribe and <strong>get “Smart Interface Design Checklists”</strong> &mdash; a <strong>free PDF deck</strong> with 150+ questions to ask yourself when designing and building almost <em>anything</em>.</p><div><section class="nlbf"><form action="//smashingmagazine.us1.list-manage.com/subscribe/post?u=16b832d9ad4b28edf261f34df&amp;id=a1666656e0" method="post"><div class="nlbwrapper"><label for="mce-EMAIL-hp" class="sr-only">Your (smashing) email</label><div class="nlbgroup"><input type="email" name="EMAIL" class="nlbf-email" id="mce-EMAIL-hp" placeholder="Your email">
<input type="submit" value="Meow!" name="subscribe" class="nlbf-button"></div></div></form><style>.c-garfield-the-cat .nlbwrapper{margin-bottom: 0;}.nlbf{display:flex;padding-bottom:.25em;padding-top:.5em;text-align:center;letter-spacing:-.5px;color:#fff;font-size:1.15em}.nlbgroup:hover{box-shadow:0 1px 7px -5px rgba(50,50,93,.25),0 3px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025)}.nlbf .nlbf-button,.nlbf .nlbf-email{flex-grow:1;flex-shrink:0;width:auto;margin:0;padding:.75em 1em;border:0;border-radius:11px;background:#fff;font-size:1em;box-shadow:none}.promo-box .nlbf-button:focus,.promo-box input.nlbf-email:active,.promo-box input.nlbf-email:focus{box-shadow:none}.nlbf-button:-ms-input-placeholder,.nlbf-email:-ms-input-placeholder{color:#777;font-style:italic}.nlbf-button::-webkit-input-placeholder,.nlbf-email::-webkit-input-placeholder{color:#777;font-style:italic}.nlbf-button:-ms-input-placeholder,.nlbf-button::-moz-placeholder,.nlbf-button::placeholder,.nlbf-email:-ms-input-placeholder,.nlbf-email::-moz-placeholder,.nlbf-email::placeholder{color:#777;font-style:italic}.nlbf .nlbf-button{transition:all .2s ease-in-out;color:#fff;background-color:#0168b8;font-weight:700;box-shadow:0 1px 1px rgba(0,0,0,.3);width:100%;border:0;border-left:1px solid #ddd;flex:2;border-top-left-radius:0;border-bottom-left-radius:0}.nlbf .nlbf-email{border-top-right-radius:0;border-bottom-right-radius:0;width:100%;flex:4;min-width:150px}@media all and (max-width:650px){.nlbf .nlbgroup{flex-wrap:wrap;box-shadow:none}.nlbf .nlbf-button,.nlbf .nlbf-email{border-radius:11px;border-left:none}.nlbf .nlbf-email{box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);min-width:100%}.nlbf .nlbf-button{margin-top:1em;box-shadow:0 1px 1px rgba(0,0,0,.5)}}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus,.nlbf .nlbf-button:hover{cursor:pointer;color:#fff;background-color:#0168b8;border-color:#dadada;box-shadow:0 1px 1px rgba(0,0,0,.3)}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus{outline:0!important;text-shadow:1px 1px 1px rgba(0,0,0,.3);box-shadow:inset 0 3px 3px rgba(0,0,0,.3)}.nlbgroup{display:flex;box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);border-radius:11px;transition:box-shadow .2s ease-in-out}.nlbwrapper{display:flex;flex-direction:column;justify-content:center}.nlbf form{width:100%}.nlbf .nlbgroup{margin:0}.nlbcaption{font-size:.9em;line-height:1.5em;color:#fff;border-radius:11px;padding:.5em 1em;display:inline-block;background-color:#0067b859;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.wf-loaded-stage2 .nlbf .nlbf-button{font-family:Mija}.mts{margin-top: 5px !important;}.mbn{margin-bottom: 0 !important;}</style></section><p class="mts mbn"><small class="promo-box__footer mtm block grey"><em>Once a week. Useful tips on <a href="https://www.smashingmagazine.com/the-smashing-newsletter/">front-end &amp; UX</a>. Trusted by 207.000 friendly folks.</em></small></p></div></p>
</div>
</div>
<div class="feature-panel-right-col">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-firechat.svg"
    alt="Feature Panel"
    width="310"
    height="400"
/>

</div>

<p></div>
</aside>
</div></p>

<h2 id="defining-a-store">Defining A Store</h2>

<p>Before we can do anything with a Vuex store, we need to define one. In Vuex 4, a store definition will look like this:</p>

<pre><code class="language-javascript">import { createStore } from 'vuex'

export const counterStore = createStore({
  state: {
    count: 0
  },
  
  getters: {
    double (state) {
      return state.count * 2
    }
  },
  
  mutations: {
    increment (state) {
      state.count++
    }
  },
  
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})</code></pre>

<p>Each store has four parts: <code>state</code> stores the data, <code>getters</code> give you computed state, <code>mutations</code> are used to mutate the state, and <code>actions</code> are the methods that are called from outside the store to accomplish anything related to the store. Usually, actions don’t just commit a mutation as this example shows. Instead, they are used to do asynchronous tasks because mutations <em>must</em> be synchronous or they just implement more complicated or multi-step functionality. Actions also cannot mutate the state on their own; they must use a mutator. So what does Vuex 5 look like?</p>

<pre><code class="language-javascript">import { defineStore } from 'vuex'

export const counterStore = defineStore({
  name: 'counter',
  
  state() {
    return { count: 0 }
  },
  
  getters: {
    double () {
      return this.count * 2
    }
  },
  
  actions: {
    increment () {
      this.count++
    }
  }
})</code></pre>

<p>There are a few changes to note here. First, instead of <code>createStore</code>, we use <code>defineStore</code>. This difference is negligible, but it’s there for semantic reasons, which we’ll go over later. Next, we need to provide a <code>name</code> for the store, which we didn’t need before. In the past, modules got their own name, but they weren’t provided by the module itself; they were just the property name they were assigned to by the parent store that added them. Now, there are <em>no modules</em>. Instead, each module will be a separate store and have a name. This name is used by the Vuex registry, which we’ll talk about later.</p>

<p>After that, we need to make <code>state</code> a function that returns the initial state instead of just setting it to the initial state. This is similar to the <code>data</code> option on components. We write <code>getters</code> very similar to the way we did in Vuex 4, but instead of using the <code>state</code> as a parameter for each getter, you can just use <code>this</code> to get to the state. In the same way, <code>actions</code> don’t need to worry about a <code>context</code> object being passed in: they can just use <code>this</code> to access everything. Finally, there are no <code>mutations</code>. Instead, mutations are combined with <code>actions</code>. Kia <a href="https://youtu.be/dQ5btkDrdJ8?t=505">noted</a> that too often, mutations just became simple setters, making them pointlessly verbose, so they removed them. He didn’t mention whether it was “ok” to mutate the state directly from outside the store, but we are definitely allowed and encouraged to mutate state directly from an action and the Flux pattern frowns on the direct mutation of state.</p>

<p><strong>Note</strong>: <em>For those who prefer the composition API over the options API for creating components, you’ll be happy to learn there is also a way to create stores in a similar fashion to using the composition API.</em></p>

<pre><code class="language-javascript">import { ref, computed } from 'vue'
import { defineStore } from 'vuex'

export const counterStore = defineStore('counter', {
  const count = ref(0)

  const double = computed(() => count.value * 2)
  
  function increment () {
    count.value++
  }

  return { count, double, increment }  
})</code></pre>

<p>As shown above, the name gets passed in as the first argument for <code>defineStore</code>. The rest looks just like a composition function for components. This will yield exactly the same result as the previous example that used the options API.</p>

<div class="partners__lead-place"></div>

<h2 id="getting-the-store-instantiated">Getting The Store Instantiated</h2>

<p>In Vuex 4, things have changed from Vuex 3, but I’ll just look at v4 to keep things from getting out of hand. In v4, when you called <code>createStore</code>, you already instantiated it. You can then just use it in your app, either via <code>app.use</code> or directly:</p>

<pre><code class="language-javascript">import { createApp } from 'vue'
import App from './App.vue' // Your root component
import store from './store' // The store definition from earlier

const app = createApp(App)

app.use(store)
app.mount('#app')

// Now all your components can access it via `this.$store`
// Or you can use in composition components with `useStore()`

// -----------------------------------------------

// Or use directly... this is generally discouraged
import store from './store'

store.state.count // -&gt; 0
store.commit('increment')
store.dispatch('increment')
store.getters.double // -&gt; 4</code></pre>

<p>This is one thing that Vuex 5 makes a bit more complicated than in v4. Each app now can get a separate instance of Vuex, which makes sure that each app can have separate instances of the same stores without sharing data between them. You can share an instance of Vuex if you want to share instances of stores between apps.</p>

<pre><code class="language-javascript">import { createApp } from 'vue'
import { createVuex } from 'vuex'
import App from './App.vue' // Your root component

const app = createApp(App)
const vuex = createVuex() // create instance of Vuex

app.use(vuex) // use the instance
app.mount('#app')</code></pre>

<p>Now all of your components have access to the Vuex instance. Instead of giving your store(s) definition directly, you then import them into the components you want to use them in and use the Vuex instance to instantiate and register them:</p>

<pre><code class="language-javascript">import { defineComponent } from 'vue'
import store from './store'

export default defineComponent({
  name: 'App',

  computed: {
    counter () {
      return this.$vuex.store(store)
    }
  }
})</code></pre>

<p>Calling <code>$vuex.store</code>, instantiates and registers the store in the Vuex instance. From that point on, any time you use <code>$vuex.store</code> on that store, it’ll give you back the already instantiated store instead of instantiating it again. You can call the <code>store</code> method straight on an instance of Vuex created by <code>createVuex()</code>.</p>

<p>Now your store is accessible on that component via <code>this.counter</code>. If you’re using the composition API for your component, you can use <code>useStore</code> instead of <code>this.$vuex.store</code>:</p>

<pre><code class="language-javascript">import { defineComponent } from 'vue'
import { useStore } from 'vuex' // import useStore
import store from './store'

export default defineComponent({
  setup () {
    const counter = useStore(store)

    return { counter }
  }
})</code></pre>

<p>There are pros and cons to importing the store directly into the component and instantiating it there. It allows you to code split and lazily loads the store only where it’s needed, but now it’s a direct dependency instead of being injected by a parent (not to mention you need to import it every time you want to use it). If you want to use dependency injection to provide it throughout the app, especially if you know it’ll be used at the root of the app where code splitting won’t help, then you can just use <code>provide</code>:</p>

<pre><code class="language-javascript">import { createApp } from 'vue'
import { createVuex } from 'vuex'
import App from './App.vue'
import store from './store'

const app = createApp(App)
const vuex = createVuex()

app.use(vuex)
app.provide('store', store) // provide the store to all components
app.mount('#app')</code></pre>

<p>And you can just inject it in any component where you’re going to use it:</p>

<pre><code class="language-javascript">import { defineComponent } from 'vue'

export default defineComponent({
  name: 'App',
  inject: ['store']
})

// Or with Composition API

import { defineComponent, inject } from 'vue'

export default defineComponent({
  setup () {
    const store = inject('store')

    return { store }
  }
})</code></pre>

<p>I’m not excited about this extra verbosity, but it is more explicit and more flexible, which I am a fan of. This type of code is generally written once right away at the beginning of the project and then it doesn’t bother you again, though now you’ll either need to provide each new store or import it every time you wish to use it, but importing or injecting code modules is how we generally have to work with anything else, so it’s just making Vuex work more along the lines of how people already tend to work.</p>

<h2 id="using-a-store">Using A Store</h2>

<p>Apart from being a fan of the flexibility and the new way of defining stores the same way as a component using the composition API, there’s one more thing that makes me more excited than everything else: how stores are used. Here’s what it looks like to use a store in Vuex 4.</p>

<pre><code class="language-javascript">store.state.count            // Access State
store.getters.double         // Access Getters
store.commit('increment')    // Mutate State
store.dispatch('increment')  // Run Actions</code></pre>

<p><code>State</code>, <code>getters</code>, <code>mutations</code>, and <code>actions</code> are all handled in different ways via different properties or methods. This has the advantage of explicitness, which I praised earlier, but this explicitness doesn’t really gain us anything. And this API only gets more difficult to use when you are using namespaced modules. By comparison, Vuex 5 looks to work exactly how you would normally hope:</p>

<pre><code class="language-javascript">store.count        // Access State
store.double       // Access Getters (transparent)
store.increment()  // Run actions
// No Mutators</code></pre>

<p>Everything — the state, getters and actions — is available directly at the root of the store, making it simple to use with a lot less verbosity and practically removes all need for using <code>mapState</code>, <code>mapGetters</code>, <code>mapActions</code> and <code>mapMutations</code> for the options API or for writing <a href="https://next.vuex.vuejs.org/guide/composition-api.html#accessing-state-and-getters">extra <code>computed</code> statements or simple functions for composition API</a>. This simply makes a Vuex store look and act just like a normal store that you would build yourself, but it gets all the benefits of plugins, debugging tools, official documentation, etc.</p>

<h2 id="composing-stores">Composing Stores</h2>

<p>The final aspect of Vuex 5 we’ll look at today is composability. Vuex 5 doesn’t have namespaced modules that are all accessible from the single store. Each of those modules would be split into a completely separate store. That’s simple enough to deal with for components: they just import whichever stores they need and fire them up and use them. But what if one store wants to interact with another store? In v4, the namespacing convolutes the whole thing, so you need to use the namespace in your <code>commit</code> and <code>dispatch</code> calls, use <code>rootGetters</code> and <code>rootState</code> and then work your way up into the namespaces you want to access getters and state from. Here’s how it works in Vuex 5:</p>

<div class="break-out">

<pre><code class="language-javascript">// store/greeter.js
import { defineStore } from 'vuex'

export default defineStore({
  name: 'greeter',
  state () {
    return { greeting: 'Hello' }
  }
})

// store/counter.js
import { defineStore } from 'vuex'
import greeterStore from './greeter' // Import the store you want to interact with

export default defineStore({
  name: 'counter',

  // Then `use` the store
  use () {
    return { greeter: greeterStore }
  },
  
  state () {
    return { count: 0 }
  },
  
  getters: {
    greetingCount () {
      return `${this.greeter.greeting} ${this.count}' // access it from this.greeter
    }
  }
})</code></pre>

</div>

<p>With v5, we import the store we wish to use, then register it with <code>use</code> and now it’s accessible all over the store at whatever property name you gave it. Things are even simpler if you’re using the composition API variation of the store definition:</p>

<div class="break-out">

<pre><code class="language-javascript">// store/counter.js
import { ref, computed } from 'vue'
import { defineStore } from 'vuex'
import greeterStore from './greeter' // Import the store you want to interact with

export default defineStore('counter', ({use}) =&gt; { // `use` is passed in to function
  const greeter = use(greeterStore) // use `use` and now you have full access
  const count = 0

  const greetingCount = computed(() =&gt; {
    return  `${greeter.greeting} ${this.count}` // access it like any other variable
  })

  return { count, greetingCount }
})</code></pre>

</div>

<p>No more namespaced modules. Each store is separate and is used separately. You can use <code>use</code> to make a store available inside another store to compose them. In both examples, <code>use</code> is basically just the same mechanism as <code>vuex.store</code> from earlier and they ensure that we instantiating the stores with the correct instance of Vuex.</p>

<h2 id="typescript-support">TypeScript Support</h2>

<p>For TypeScript users, one of the greatest aspects of Vuex 5 is that the simplification made it simpler to add types to everything. The layers of abstraction that older versions of Vuex had made it nearly impossible and right now, with Vuex 4, they increased our ability to use types, but there is still too much manual work to get a decent amount of type support, whereas in v5, you can put your types inline, just as you would hope and expect.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Vuex 5 looks to be almost exactly what I — and likely many others — hoped it would be, and I feel it can’t come soon enough. It simplifies most of Vuex, removing some of the mental overhead involved, and only gets more complicated or verbose where it adds flexibility. Leave comments below about what you think of these changes and what changes you might make instead or in addition. Or go straight to the source and <a href="https://github.com/vuejs/rfcs/pulls">add an RFC</a> (Request for Comments) to the list to see what the core team thinks.</p>

<div class="partners__lead-place"></div>

<h3 id="further-reading">Further Reading</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2023/03/building-complex-forms-vue/">Building Complex Forms In Vue</a></li>
<li><a href="https://www.smashingmagazine.com/2023/05/big-tooling-upgrades-large-organizations/">How To Deal With Big Tooling Upgrades In Large Organizations</a></li>
<li><a href="https://www.smashingmagazine.com/2024/10/build-static-rss-reader-fight-fomo/">Build A Static RSS Reader To Fight Your Inner FOMO</a></li>
<li><a href="https://www.smashingmagazine.com/2023/12/marketing-changed-oop-javascript/">How Marketing Changed OOP In JavaScript</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(ra, yk, il, mrn)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item></channel></rss>