<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Michael Hoffmann]]></title><description><![CDATA[I blog about software development, career and freelancing. Typcial topics are JavaScript, TypeScript, Vue.js, Spring Boot, HTML5, CSS3 and more.]]></description><link>https://blog.mokkapps.de</link><generator>RSS for Node</generator><lastBuildDate>Sun, 12 Apr 2026 14:02:26 GMT</lastBuildDate><atom:link href="https://blog.mokkapps.de/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Create a Table of Contents With Active States in Nuxt 3]]></title><description><![CDATA[I'm a big fan of a table of contents (ToC) on the side of a blog post page, especially if it is a long article. It helps me gauge the article's length and allows me to navigate between the sections quickly.
In this article, I will show you how to cre...]]></description><link>https://blog.mokkapps.de/create-a-table-of-contents-with-active-states-in-nuxt-3</link><guid isPermaLink="true">https://blog.mokkapps.de/create-a-table-of-contents-with-active-states-in-nuxt-3</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[Nuxt]]></category><category><![CDATA[Nuxt.js]]></category><category><![CDATA[vue]]></category><category><![CDATA[Frontend Development]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Wed, 07 Dec 2022 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1670486090901/Pgbh8T9z7.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm a big fan of a table of contents (ToC) on the side of a blog post page, especially if it is a long article. It helps me gauge the article's length and allows me to navigate between the sections quickly.</p>
<p>In this article, I will show you how to create a sticky table of contents sidebar with an active state based on the current scroll position using <a target="_blank" href="https://nuxt.com/">Nuxt 3</a>, <a target="_blank" href="https://nuxt.com/modules/content">Nuxt Content</a> and <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">Intersection Observer</a>.</p>
<h2 id="heading-demo">Demo</h2>
<p>The following StackBlitz contains the source code that is used in the following chapters:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://stackblitz.com/edit/nuxt-content-table-of-contents-demo?embed=1">https://stackblitz.com/edit/nuxt-content-table-of-contents-demo?embed=1</a></div>
<h2 id="heading-setup">Setup</h2>
<p>For this demo, we need to <a target="_blank" href="https://nuxt.com/docs/getting-started/installation">initialize a Nuxt 3</a> project and install the <a target="_blank" href="https://content.nuxtjs.org/get-started">Nuxt Content</a> and <a target="_blank" href="https://tailwindcss.nuxt.dev/getting-started/setup">Nuxt Tailwind</a> (optional) modules.</p>
<p>We need to add these modules to <code>nuxt.config.ts</code>:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineNuxtConfig({
  modules: [<span class="hljs-string">'@nuxt/content'</span>, <span class="hljs-string">'@nuxtjs/tailwindcss'</span>],
})
</code></pre>
<p>Of course, we need some content to show the table of contents. For this demo, I will reference the <code>index.md</code> file from my <a target="_blank" href="https://stackblitz.com/edit/nuxt-content-table-of-contents-demo?file=content/index.md">StackBlitz demo</a>.</p>
<p>To render this content, let's create a <a target="_blank" href="https://nuxt.com/docs/guide/directory-structure/pages#catch-all-route">catch-all route</a> in the <code>pages</code> directory:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"p-4 flex flex-col gap-4"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">ContentDoc</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">template</span> #<span class="hljs-attr">default</span>=<span class="hljs-string">"{ doc }"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid grid-cols-12 gap-8"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"nuxt-content col-span-8"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">ContentRenderer</span> <span class="hljs-attr">ref</span>=<span class="hljs-string">"nuxtContent"</span> <span class="hljs-attr">:value</span>=<span class="hljs-string">"doc"</span> /&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">ContentDoc</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>The <code>&lt;ContentDoc&gt;</code> component fetches and renders a single document, and the <code>&lt;ContentRenderer&gt;</code> component renders the body of a Markdown document.</p>
<p><a target="_blank" href="https://content.nuxtjs.org/api/components/content-renderer">Check the official docs</a> for more information about these Nuxt Content components.</p>
<p>Now let's add a <code>TableOfContents.vue</code> component to this template:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">const</span> activeTocId = ref(<span class="hljs-literal">null</span>)
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"p-4 flex flex-col gap-4"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">ContentDoc</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">template</span> #<span class="hljs-attr">default</span>=<span class="hljs-string">"{ doc }"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid grid-cols-12 gap-8"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"nuxt-content col-span-8"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">ContentRenderer</span> <span class="hljs-attr">ref</span>=<span class="hljs-string">"nuxtContent"</span> <span class="hljs-attr">:value</span>=<span class="hljs-string">"doc"</span> /&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"col-span-4 border rounded-md p-4"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"sticky top-0 flex flex-col items-center"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">TableOfContents</span> <span class="hljs-attr">:activeTocId</span>=<span class="hljs-string">"activeTocId"</span> /&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">ContentDoc</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>I'll explain the <code>activeTocId</code> prop in the following "Intersection Observer" chapter.</p>
<p>Let's take a look at the component's code:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { Ref } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>

<span class="hljs-keyword">const</span> props = withDefaults(defineProps&lt;{ <span class="hljs-attr">activeTocId</span>: string }&gt;(), {})

<span class="hljs-keyword">const</span> router = useRouter()

<span class="hljs-keyword">const</span> { <span class="hljs-attr">data</span>: blogPost } = <span class="hljs-keyword">await</span> useAsyncData(<span class="hljs-string">`blogToc`</span>, <span class="hljs-function">() =&gt;</span> queryContent(<span class="hljs-string">`/`</span>).findOne())
<span class="hljs-keyword">const</span> tocLinks = computed(<span class="hljs-function">() =&gt;</span> blogPost.value?.body.toc.links ?? [])

<span class="hljs-keyword">const</span> onClick = <span class="hljs-function">(<span class="hljs-params">id: string</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> el = <span class="hljs-built_in">document</span>.getElementById(id)
  <span class="hljs-keyword">if</span> (el) {
    router.push({ <span class="hljs-attr">hash</span>: <span class="hljs-string">`#<span class="hljs-subst">${id}</span>`</span> })
    el.scrollIntoView({ <span class="hljs-attr">behavior</span>: <span class="hljs-string">'smooth'</span>, <span class="hljs-attr">block</span>: <span class="hljs-string">'center'</span> })
  }
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"max-h-82 overflow-auto"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h4</span>&gt;</span>Table of Contents<span class="hljs-tag">&lt;/<span class="hljs-name">h4</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">nav</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex mt-4"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"ml-0 pl-4"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">li</span>
          <span class="hljs-attr">v-for</span>=<span class="hljs-string">"{ id, text, children } in tocLinks"</span>
          <span class="hljs-attr">:id</span>=<span class="hljs-string">"`toc-${id}`"</span>
          <span class="hljs-attr">:key</span>=<span class="hljs-string">"id"</span>
          <span class="hljs-attr">class</span>=<span class="hljs-string">"cursor-pointer text-sm list-none ml-0 mb-2 last:mb-0"</span>
          @<span class="hljs-attr">click</span>=<span class="hljs-string">"onClick(id)"</span>
        &gt;</span>
          {{ text }}
          <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"children"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"ml-3 my-2"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">li</span>
              <span class="hljs-attr">v-for</span>=<span class="hljs-string">"{ id: childId, text: childText } in children"</span>
              <span class="hljs-attr">:id</span>=<span class="hljs-string">"`toc-${childId}`"</span>
              <span class="hljs-attr">:key</span>=<span class="hljs-string">"childId"</span>
              <span class="hljs-attr">class</span>=<span class="hljs-string">"cursor-pointer text-xs list-none ml-0 mb-2 last:mb-0"</span>
              @<span class="hljs-attr">click.stop</span>=<span class="hljs-string">"onClick(childId)"</span>
            &gt;</span>
              {{ childText }}
            <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>Let's analyze this code:</p>
<p>To get a list of all available headlines, we use the <a target="_blank" href="https://content.nuxtjs.org/api/composables/query-content">queryContent composable</a> and access them via <code>body.toc.links</code>:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> { data: blogPost } = <span class="hljs-keyword">await</span> useAsyncData(<span class="hljs-string">`blogToc`</span>, <span class="hljs-function">() =&gt;</span> queryContent(<span class="hljs-string">`/`</span>).findOne())
<span class="hljs-keyword">const</span> tocLinks = computed(<span class="hljs-function">() =&gt;</span> blogPost.value?.body.toc.links ?? [])
</code></pre>
<p>If someone clicks on a link in the ToC, we query the HTML element from the DOM, push the hash route and smoothly scroll the element into the viewport:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> onClick = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> el = <span class="hljs-built_in">document</span>.getElementById(id)
  <span class="hljs-keyword">if</span> (el) {
    router.push({ hash: <span class="hljs-string">`#<span class="hljs-subst">${id}</span>`</span> })
    el.scrollIntoView({ behavior: <span class="hljs-string">'smooth'</span>, block: <span class="hljs-string">'center'</span> })
  }
}
</code></pre>
<p>At this point, we can show a list of all the headlines of our content in the sidebar, but our ToC does not indicate which headline is currently visible.</p>
<h2 id="heading-intersection-observer">Intersection Observer</h2>
<p>We use the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">Intersection Observer</a> to handle detecting when an element scrolls into our viewport. It's <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#browser_compatibility">supported by almost every browser</a>.</p>
<p>Nuxt Content automatically adds an <code>id</code> to each heading of our content files. Using <code>document.querySelectorAll</code>, we query all <code>h2</code> and <code>h3</code> elements associated with an <code>id</code> and use the Intersection Observer API to get informed when they scroll into view.</p>
<p>Let's go ahead and implement that logic:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { watchDebounced } <span class="hljs-keyword">from</span> <span class="hljs-string">'@vueuse/core'</span>

<span class="hljs-keyword">const</span> activeTocId = ref(<span class="hljs-literal">null</span>)
<span class="hljs-keyword">const</span> nuxtContent = ref(<span class="hljs-literal">null</span>)

<span class="hljs-keyword">const</span> observer: Ref&lt;IntersectionObserver | <span class="hljs-literal">null</span> | <span class="hljs-literal">undefined</span>&gt; = ref(<span class="hljs-literal">null</span>)
<span class="hljs-keyword">const</span> observerOptions = reactive({
  <span class="hljs-attr">root</span>: nuxtContent.value,
  <span class="hljs-attr">threshold</span>: <span class="hljs-number">0.5</span>,
})

onMounted(<span class="hljs-function">() =&gt;</span> {
  observer.value = <span class="hljs-keyword">new</span> IntersectionObserver(<span class="hljs-function">(<span class="hljs-params">entries</span>) =&gt;</span> {
    entries.forEach(<span class="hljs-function">(<span class="hljs-params">entry</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> id = entry.target.getAttribute(<span class="hljs-string">'id'</span>)
      <span class="hljs-keyword">if</span> (entry.isIntersecting) {
        activeTocId.value = id
      }
    })
  }, observerOptions)

  <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'.nuxt-content h2[id], .nuxt-content h3[id]'</span>).forEach(<span class="hljs-function">(<span class="hljs-params">section</span>) =&gt;</span> {
    observer.value?.observe(section)
  })
})

onUnmounted(<span class="hljs-function">() =&gt;</span> {
  observer.value?.disconnect()
})
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Let's break down the single steps that are happening in this code.</p>
<p>First, we define some reactive variables:</p>
<ul>
<li><p><code>activeTocId</code> is used to track the currently active DOM element to be able to add some CSS styles to it.</p>
</li>
<li><p><code>nuxtContent</code> is a <a target="_blank" href="https://vuejs.org/guide/essentials/template-refs.html#template-refs">Template Ref</a> to access the DOM element of the <code>ContentRenderer</code> component.</p>
</li>
<li><p><code>observer</code> is used to track the <code>h2</code> and <code>h3</code> HTML elements that scroll into the viewport.</p>
</li>
<li><p><code>observerOptions</code> contains a set of options that define when the observer callback is invoked. It contains the <code>nuxtContent</code> ref as root for the observer and a threshold of 0.5, which means that if 50% of the way through the viewport is visible, the callback will fire. You can also set it to <code>0</code>; it will fire the callback if one element pixel is visible.</p>
</li>
</ul>
<p>In the <code>onMounted</code> lifecycle hook, we are initializing the observer. We iterate over each article heading and set the <code>activeTocId</code> value if the entry intersects with the viewport. We also use <code>document.querySelectorAll</code> to target our <code>.nuxt-content</code> article and get the DOM elements that are either <code>h2</code> or <code>h3</code> elements with IDs and observe those using our previously initialized <code>IntersectionObserver</code>.</p>
<p>Finally, we are disconnecting our observer in the <code>onUnmounted</code> lifecycle hook to inform the observer to no longer track these headings when we navigate away.</p>
<h2 id="heading-style-active-link">Style Active Link</h2>
<p>Let's improve the code by applying styles to the <code>activeTocId</code> element in our table of contents component. It should be highlighted and show an indicator:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { watchDebounced } <span class="hljs-keyword">from</span> <span class="hljs-string">'@vueuse/core'</span>
<span class="hljs-keyword">import</span> { Ref } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>

<span class="hljs-keyword">const</span> props = withDefaults(defineProps&lt;{ <span class="hljs-attr">activeTocId</span>: string }&gt;(), {})

<span class="hljs-keyword">const</span> router = useRouter()

<span class="hljs-keyword">const</span> sliderHeight = useState(<span class="hljs-string">'sliderHeight'</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-number">0</span>)
<span class="hljs-keyword">const</span> sliderTop = useState(<span class="hljs-string">'sliderTop'</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-number">0</span>)
<span class="hljs-keyword">const</span> tocLinksH2: Ref&lt;<span class="hljs-built_in">Array</span>&lt;HTMLElement&gt;&gt; = ref([])
<span class="hljs-keyword">const</span> tocLinksH3: Ref&lt;<span class="hljs-built_in">Array</span>&lt;HTMLElement&gt;&gt; = ref([])

<span class="hljs-keyword">const</span> { <span class="hljs-attr">data</span>: blogPost } = <span class="hljs-keyword">await</span> useAsyncData(<span class="hljs-string">`blogToc`</span>, <span class="hljs-function">() =&gt;</span> queryContent(<span class="hljs-string">`/`</span>).findOne())
<span class="hljs-keyword">const</span> tocLinks = computed(<span class="hljs-function">() =&gt;</span> blogPost.value?.body.toc.links ?? [])

<span class="hljs-keyword">const</span> onClick = <span class="hljs-function">(<span class="hljs-params">id: string</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> el = <span class="hljs-built_in">document</span>.getElementById(id)
  <span class="hljs-keyword">if</span> (el) {
    router.push({ <span class="hljs-attr">hash</span>: <span class="hljs-string">`#<span class="hljs-subst">${id}</span>`</span> })
    el.scrollIntoView({ <span class="hljs-attr">behavior</span>: <span class="hljs-string">'smooth'</span>, <span class="hljs-attr">block</span>: <span class="hljs-string">'center'</span> })
  }
}

watchDebounced(
  <span class="hljs-function">() =&gt;</span> props.activeTocId,
  <span class="hljs-function">(<span class="hljs-params">newActiveTocId</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> h2Link = tocLinksH2.value.find(<span class="hljs-function">(<span class="hljs-params">el: HTMLElement</span>) =&gt;</span> el.id === <span class="hljs-string">`toc-<span class="hljs-subst">${newActiveTocId}</span>`</span>)
    <span class="hljs-keyword">const</span> h3Link = tocLinksH3.value.find(<span class="hljs-function">(<span class="hljs-params">el: HTMLElement</span>) =&gt;</span> el.id === <span class="hljs-string">`toc-<span class="hljs-subst">${newActiveTocId}</span>`</span>)

    <span class="hljs-keyword">if</span> (h2Link) {
      sliderHeight.value = h2Link.offsetHeight
      sliderTop.value = h2Link.offsetTop - <span class="hljs-number">100</span>
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (h3Link) {
      sliderHeight.value = h3Link.offsetHeight
      sliderTop.value = h3Link.offsetTop - <span class="hljs-number">100</span>
    }
  },
  { <span class="hljs-attr">debounce</span>: <span class="hljs-number">200</span>, <span class="hljs-attr">immediate</span>: <span class="hljs-literal">true</span> }
)
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"max-h-82 overflow-auto"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h4</span>&gt;</span>Table of Contents<span class="hljs-tag">&lt;/<span class="hljs-name">h4</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">nav</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex mt-4"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"relative bg-secondary w-0.5 overflow-hidden rounded"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
          <span class="hljs-attr">class</span>=<span class="hljs-string">"
            absolute
            left-0
            w-full
            transition-all
            duration-200
            rounded
            bg-red-500
          "</span>
          <span class="hljs-attr">:style</span>=<span class="hljs-string">"{ height: `${sliderHeight}px`, top: `${sliderTop}px` }"</span>
        &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"ml-0 pl-4"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">li</span>
          <span class="hljs-attr">v-for</span>=<span class="hljs-string">"{ id, text, children } in tocLinks"</span>
          <span class="hljs-attr">:id</span>=<span class="hljs-string">"`toc-${id}`"</span>
          <span class="hljs-attr">:key</span>=<span class="hljs-string">"id"</span>
          <span class="hljs-attr">ref</span>=<span class="hljs-string">"tocLinksH2"</span>
          <span class="hljs-attr">class</span>=<span class="hljs-string">"cursor-pointer text-sm list-none ml-0 mb-2 last:mb-0"</span>
          <span class="hljs-attr">:class</span>=<span class="hljs-string">"{ 'font-bold': id === activeTocId }"</span>
          @<span class="hljs-attr">click</span>=<span class="hljs-string">"onClick(id)"</span>
        &gt;</span>
          {{ text }}
          <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"children"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"ml-3 my-2"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">li</span>
              <span class="hljs-attr">v-for</span>=<span class="hljs-string">"{ id: childId, text: childText } in children"</span>
              <span class="hljs-attr">:id</span>=<span class="hljs-string">"`toc-${childId}`"</span>
              <span class="hljs-attr">:key</span>=<span class="hljs-string">"childId"</span>
              <span class="hljs-attr">ref</span>=<span class="hljs-string">"tocLinksH3"</span>
              <span class="hljs-attr">class</span>=<span class="hljs-string">"cursor-pointer text-xs list-none ml-0 mb-2 last:mb-0"</span>
              <span class="hljs-attr">:class</span>=<span class="hljs-string">"{ 'font-bold': childId === activeTocId }"</span>
              @<span class="hljs-attr">click.stop</span>=<span class="hljs-string">"onClick(childId)"</span>
            &gt;</span>
              {{ childText }}
            <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>We use the <a target="_blank" href="https://vueuse.org/shared/watchdebounced/#watchdebounced">VueUse's watchDebounced composable</a> to debounced watch changes of the active ToC element ID:</p>
<pre><code class="lang-ts">watchDebounced(
  <span class="hljs-function">() =&gt;</span> props.activeTocId,
  <span class="hljs-function">(<span class="hljs-params">newActiveTocId</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> h2Link = tocLinksH2.value.find(<span class="hljs-function">(<span class="hljs-params">el: HTMLElement</span>) =&gt;</span> el.id === <span class="hljs-string">`toc-<span class="hljs-subst">${newActiveTocId}</span>`</span>)
    <span class="hljs-keyword">const</span> h3Link = tocLinksH3.value.find(<span class="hljs-function">(<span class="hljs-params">el: HTMLElement</span>) =&gt;</span> el.id === <span class="hljs-string">`toc-<span class="hljs-subst">${newActiveTocId}</span>`</span>)

    <span class="hljs-keyword">if</span> (h2Link) {
      sliderHeight.value = h2Link.offsetHeight
      sliderTop.value = h2Link.offsetTop - <span class="hljs-number">100</span>
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (h3Link) {
      sliderHeight.value = h3Link.offsetHeight
      sliderTop.value = h3Link.offsetTop - <span class="hljs-number">100</span>
    }
  },
  { debounce: <span class="hljs-number">200</span>, immediate: <span class="hljs-literal">true</span> }
)
</code></pre>
<p>Based on the current active ToC element ID, we find the HTML element from the list of available links and set the slider height &amp; top values accordingly.</p>
<p>Check the <a target="_blank" href="https://mokkapps.de/blog/create-a-table-of-contents-with-active-states-in-nuxt-3#demo">StackBlitz demo</a> for the full source code and to play around with this implementation. A similar ToC is also available on my <a target="_blank" href="https://mokkapps.de/blog">blog</a>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I'm pleased with my table of contents implementation using Nuxt 3, Nuxt Content, and Intersection Observer.</p>
<p>Of course, you can use the Intersection Observer in a traditional Vue application without Nuxt. The Intersection Observer API is mighty and can also be used to implement features like <a target="_blank" href="https://www.webtips.dev/how-to-lazy-load-images-with-intersection-observer">lazy-loading images</a>.</p>
<p>Leave a comment if you have a better solution to implement such a ToC.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Polite Newsletter Popup With Nuxt 3]]></title><description><![CDATA[This year I launched a free eBook with 27 helpful tips for Vue developers for subscribers of my weekly Vue newsletter. For marketing purposes, I showed a popup on the landing page of my portfolio page each time a user visited my site. I was aware tha...]]></description><link>https://blog.mokkapps.de/building-a-polite-newsletter-popup-with-nuxt-3</link><guid isPermaLink="true">https://blog.mokkapps.de/building-a-polite-newsletter-popup-with-nuxt-3</guid><category><![CDATA[Nuxt]]></category><category><![CDATA[Nuxt.js]]></category><category><![CDATA[Vue.js]]></category><category><![CDATA[vue]]></category><category><![CDATA[Frontend Development]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Tue, 18 Oct 2022 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1666623193775/cZK4XcJua.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This year I launched a <a target="_blank" href="https://mokkapps.de/ebook/27-helpful-tips-for-vue-developers">free eBook with 27 helpful tips for Vue developers</a> for subscribers of my <a target="_blank" href="https://weekly-vue.news">weekly Vue newsletter</a>. For marketing purposes, I showed a popup on the landing page of my <a target="_blank" href="https://mokkapps.de">portfolio page</a> each time a user visited my site. I was aware that users probably could get annoyed by that popup. Thus I added a "Don't show again" button to that popup. I thought I solved the problem!</p>
<p>But soon, I realized that many other sites solved that problem more elegantly. If you visit their website, stay for some time, and scroll through the content, a small notification appears at the bottom of the screen. It asks if you are interested in a specific product, and if you agree, it redirects to a page with information about this product.</p>
<p>In this article, I'll explain how I built a polite popup to ask people if they would like to subscribe to <a target="_blank" href="https://weekly-vue.news">my newsletter</a> using Nuxt 3.</p>
<h2 id="heading-what-is-a-polite-popup">What is a polite popup?</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1666622901064/XR-rGrzjmm.jpeg" alt="Photo by Emily Morter on Unsplash" /></p>
<p>The goal of a so-called polite popup is to only ask for visitors emails if it detects that visitors are engaged with your content. This means they’ll be more likely to sign up by the time we ask them because it’ll be <strong>after</strong> they’ve decided they liked our content.</p>
<p>In the following sections, we'll build a popup that</p>
<ul>
<li>waits for a visitor to browse the website</li>
<li>makes sure visitors are interested in the website</li>
<li>appears off to the side in a non-intrusive way</li>
<li>is easy to dismiss or ignore</li>
<li>asks for permission first</li>
<li>waits a bit before it appears again</li>
</ul>
<h2 id="heading-implementation">Implementation</h2>
<p>Now that we know the criteria of a polite popup let's start implementing it using <a target="_blank" href="https://v3.nuxtjs.org/">Nuxt 3</a>.</p>
<p>I use Nuxt.js in this example, but the concepts and solutions are not tied to any framework.</p>
<p>The demo code is interactively available at StackBlitz:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://stackblitz.com/edit/polite-popup-nuxt-3?ctl=1&amp;embed=1&amp;file=README.md">https://stackblitz.com/edit/polite-popup-nuxt-3?ctl=1&amp;embed=1&amp;file=README.md</a></div>
<h3 id="heading-implement-the-composable">Implement the composable</h3>
<p>The most exciting challenge of the polite popup is only showing it if visitors are interested in the website and content we want to promote.</p>
<p>Technically, we'll solve it this way:</p>
<ul>
<li>The visitor must be <strong>visiting a page with Vue-related content</strong> as my newsletter targets Vue developers.</li>
<li>The visitor must be actively scrolling the current page for <strong>6 seconds or more</strong>.</li>
<li>The visitor must scroll through at least <strong>35% of the current page</strong> during their visit.</li>
</ul>
<p>If these numbers aren’t generating the amount of engagement you want to see from visitors, you can enable a more <strong>aggressive</strong> mode that will lower the threshold by about 20-30%.</p>
<p>Let's start by writing a Vue composable for our polite popup:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { useWindowScroll, useWindowSize, useTimeoutFn } <span class="hljs-keyword">from</span> <span class="hljs-string">'@vueuse/core'</span>

<span class="hljs-keyword">const</span> config = {
  timeoutInMs: <span class="hljs-number">3000</span>,
  contentScrollThresholdInPercentage: <span class="hljs-number">300</span>,
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> usePolitePopup = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> visible = useState(<span class="hljs-string">'visible'</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-literal">false</span>)
  <span class="hljs-keyword">const</span> readTimeElapsed = useState(<span class="hljs-string">'read-time-elapsed'</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-literal">false</span>)

  <span class="hljs-keyword">const</span> { start } = useTimeoutFn(
    <span class="hljs-function">() =&gt;</span> {
      readTimeElapsed.value = <span class="hljs-literal">true</span>
    },
    config.timeoutInMs,
    { immediate: <span class="hljs-literal">false</span> }
  )
  <span class="hljs-keyword">const</span> { y: scrollYInPx } = useWindowScroll()
  <span class="hljs-keyword">const</span> { height: windowHeight } = useWindowSize()

  <span class="hljs-comment">// Returns percentage scrolled (ie: 80 or NaN if trackLength == 0)</span>
  <span class="hljs-keyword">const</span> amountScrolledInPercentage = computed(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> documentScrollHeight = <span class="hljs-built_in">document</span>.documentElement.scrollHeight
    <span class="hljs-keyword">const</span> trackLength = documentScrollHeight - windowHeight.value
    <span class="hljs-keyword">const</span> percentageScrolled = <span class="hljs-built_in">Math</span>.floor((scrollYInPx.value / trackLength) * <span class="hljs-number">100</span>)
    <span class="hljs-keyword">return</span> percentageScrolled
  })

  <span class="hljs-keyword">const</span> scrolledContent = computed(<span class="hljs-function">() =&gt;</span> amountScrolledInPercentage.value &gt;= config.contentScrollThresholdInPercentage)

  <span class="hljs-keyword">const</span> trigger = <span class="hljs-function">() =&gt;</span> {
    readTimeElapsed.value = <span class="hljs-literal">false</span>
    start()
  }

  watch([readTimeElapsed, scrolledContent], <span class="hljs-function">(<span class="hljs-params">[newReadTimeElapsed, newScrolledContent]</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (newReadTimeElapsed &amp;&amp; newScrolledContent) {
      visible.value = <span class="hljs-literal">true</span>
    }
  })

  <span class="hljs-keyword">return</span> {
    visible,
    trigger,
  }
}
</code></pre>
<p>We defined two state variables:</p>
<ul>
<li><code>visible</code>: a boolean indicating if the popup should be visible or not.</li>
<li><code>readTimeElapsed</code>: a boolean indicating if the user has spent the defined time on the page.</li>
</ul>
<p>The <code>trigger</code> method is exposed and triggers the a timer which is used to check if the visitor has spent a predefined amount of time on the page. A Vue watcher is used to set <code>visible</code> to <code>true</code> if the timer has expired and the scroll threshold is exceeded.
For the timer, we use <a target="_blank" href="https://vueuse.org/shared/usetimeoutfn">VueUse's useTimeoutFn composable</a> which runs a <code>setTimeout</code> function and sets the <code>readTimeElapsed</code> state variable to <code>true</code> after the timer has expired.</p>
<p>Let's take a detailed look at the <code>amountScrolledInPercentage</code> computed property:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { useWindowSize } <span class="hljs-keyword">from</span> <span class="hljs-string">'@vueuse/core'</span>

<span class="hljs-keyword">const</span> { height: windowHeight } = useWindowSize()

<span class="hljs-comment">// Returns percentage scrolled (ie: 80 or NaN if trackLength == 0)</span>
<span class="hljs-keyword">const</span> amountScrolledInPercentage = computed(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> documentScrollHeight = <span class="hljs-built_in">document</span>.documentElement.scrollHeight
  <span class="hljs-keyword">const</span> trackLength = documentScrollHeight - windowHeight.value
  <span class="hljs-keyword">const</span> percentageScrolled = <span class="hljs-built_in">Math</span>.floor((scrollYInPx.value / trackLength) * <span class="hljs-number">100</span>)
  <span class="hljs-keyword">return</span> percentageScrolled
})
</code></pre>
<p>To get the total scrollable area of a document, we need to retrieve the following two measurements of the page:</p>
<ol>
<li><strong>The height of the browser window</strong>: We use <a target="_blank" href="https://vueuse.org/core/usewindowsize/">VueUse's useWindowSize composable</a> to get reactive variable of the browser window height.</li>
<li><strong>The height of the entire document</strong>: We use <code>document.documentElement.scrollHeight</code> to get the height of the document, including content not visible on the screen due to overflow.</li>
</ol>
<p>By subtracting 2 from 1, we get the total scrollable area of the document. <a target="_blank" href="https://vueuse.org/core/usewindowscroll">VueUse's useWindowScroll composable</a> is used to access the number of pixels the document is currently scrolled along the vertical axis.</p>
<p>Move your eyes down to the <code>trackLength</code> variable, which gets the total available scroll length of the document. The variable will contain 0 if the page is <strong>not</strong> scrollable. The <code>percentageScrolled</code> variable then divides the <code>scrollYInPx</code> variable (amount the user has scrolled) with <code>trackLength</code> to derive how much the user has scrolled percentage wise.</p>
<p>Finally, we need to trigger the popup on certain pages. In our case, we only want to trigger it on the Vue route path:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;main&gt;
    &lt;ContentDoc /&gt;
  &lt;/main&gt;
&lt;/template&gt;

&lt;script setup lang="ts"&gt;
const route = useRoute()

const { trigger } = usePolitePopup()

if (route.path === '/vue') {
  trigger()
}
&lt;/script&gt;
</code></pre>
<h3 id="heading-write-the-popup-component">Write the popup component</h3>
<p>Now it's time to write the Vue component that renders the popup:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div v-if="visible" class="fixed z-50 right-0 bottom-0 md:right-5 md:bottom-5 p-4 rounded-md bg-white shadow-lg"&gt;
    &lt;span&gt;May I show you something cool?&lt;/span&gt;
    &lt;div class="flex gap-4 mt-4"&gt;
      &lt;button @click="onClickOk"&gt;OK&lt;/button&gt;
      &lt;button @click="onClickClose"&gt;Nah, thanks&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script setup lang="ts"&gt;
const { setClosed, visible } = usePolitePopup()

const onClickOk = async () =&gt; {
  setClosed()
  navigateTo('/newsletter')
}

const onClickClose = () =&gt; {
  setClosed()
}
&lt;/script&gt;
</code></pre>
<p>In this example, I'm using <a target="_blank" href="https://tailwindcss.nuxtjs.org/">Nuxt Tailwind</a> to style the component.</p>
<p><code>PolitePopup.vue</code> is rendered at a fixed position of the viewport if the <code>visible</code> reactive variable value is <code>true</code>. It offers two buttons, one to accept the offer and one to decline it.</p>
<p>As you can see, we are using <code>setClosed</code> from our <code>useShowPopup</code> composable, which we haven't defined yet. Let's define it:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> usePolitePopup = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> visible = useState(<span class="hljs-string">'visible'</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-literal">false</span>)
  ...

  <span class="hljs-keyword">const</span> setClosed = <span class="hljs-function">() =&gt;</span> {
    visible.value = <span class="hljs-literal">false</span>
  }

  <span class="hljs-keyword">return</span> {
    ...
    setClosed
  }
}
</code></pre>
<p>The last step is to add our new <code>PolitePopup.vue</code> component to the template of <code>app.vue</code>:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;NuxtLayout&gt;
      &lt;NuxtPage /&gt;
    &lt;/NuxtLayout&gt;
    &lt;PolitePopup /&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>
<p>At this point, we finished the basic implementation of the polite popup. If we navigate to <code>/vue</code>, spend 3 seconds on the page, and scroll down more than 300 pixel the polite popup appears:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1666622902504/zqWqNC68qa.png" alt="Polite Popup Demo" /></p>
<h3 id="heading-add-logic-to-wait-a-bit-before-the-popup-appears-again">Add logic to wait a bit before the popup appears again</h3>
<p>One problem with the current implementation: each time we reload the page and scroll down, the popup is triggered. But our popup should wait a bit before it appears again. So let's implement that logic.</p>
<p>First, we need a way to store the status of the polite popup in LocalStorage. For this, we use the following data model:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">interface</span> PolitePopupStorageDTO {
  status: <span class="hljs-string">'unsubscribed'</span> | <span class="hljs-string">'subscribed'</span>
  seenCount: <span class="hljs-built_in">number</span>
  lastSeenAt: <span class="hljs-built_in">number</span>
}
</code></pre>
<ul>
<li><code>status</code> is per default <code>unsubscribed</code> and is set to <code>subscribed</code> if a visitor subscribes to the newsletter.</li>
<li><code>seenCount</code> tracks how often the user has seen the popup.</li>
<li><code>lastSeenAt</code> tracks the timestamp when the visitor has seen the popup.</li>
</ul>
<p>Let's add that interface to our <code>usePolitePopup</code> composable:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">interface</span> PolitePopupStorageDTO {
  status: <span class="hljs-string">'unsubscribed'</span> | <span class="hljs-string">'subscribed'</span>
  seenCount: <span class="hljs-built_in">number</span>
  lastSeenAt: <span class="hljs-built_in">number</span>
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> usePolitePopup = <span class="hljs-function">() =&gt;</span> {
  ...
  <span class="hljs-keyword">const</span> storedData: Ref&lt;PolitePopupStorageDTO&gt; = useLocalStorage(<span class="hljs-string">'polite-popup'</span>, {
    status: <span class="hljs-string">'unsubscribed'</span>,
    seenCount: <span class="hljs-number">0</span>,
    lastSeenAt: <span class="hljs-number">0</span>,
  })
  ...
  watch(
    [readTimeElapsed, scrolledContent],
    <span class="hljs-function">(<span class="hljs-params">[newReadTimeElapsed, newScrolledContent]</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (newReadTimeElapsed &amp;&amp; newScrolledContent) {
        visible.value = <span class="hljs-literal">true</span>;
        storedData.value.seenCount += <span class="hljs-number">1</span>;
        storedData.value.lastSeenAt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getTime();
      }
    }
  );
  ...

  <span class="hljs-keyword">return</span> {
    ...
  }
}
</code></pre>
<p>We use <a target="_blank" href="https://vueuse.org/core/uselocalstorage/#uselocalstorage">VueUse's useLocalStorage composable</a> to get a reactive variable of a LocalStorage entry. Each time our watcher is fired and set the popup visible, we increment <code>seenCount</code> and set the current timestamp at <code>lastSeenAt</code> in our LocalStorage object.</p>
<p>Let's store the information that the visitor has subscribed to the newsletter. Let's add that logic to <code>newsletter.vue</code>:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;main class="flex flex-col gap-8"&gt;
    &lt;NuxtLink to="/"&gt;Back home&lt;/NuxtLink&gt;
    &lt;button @click="setSubscribed"&gt;Subscribe&lt;/button&gt;
  &lt;/main&gt;
&lt;/template&gt;

&lt;script setup lang="ts"&gt;
const { setSubscribed } = usePolitePopup()
&lt;/script&gt;
</code></pre>
<p>and in</p>
<pre><code class="lang-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> usePolitePopup = <span class="hljs-function">() =&gt;</span> {
  ...

  <span class="hljs-keyword">const</span> setSubscribed = <span class="hljs-function">() =&gt;</span> {
    storedData.value.status = <span class="hljs-string">'subscribed'</span>
  }

  <span class="hljs-keyword">return</span> {
    ...
    setSubscribed
  }
}
</code></pre>
<h3 id="heading-extend-visibility-logic">Extend visibility logic</h3>
<p>The next step is only to show the popup if the current visitor</p>
<ul>
<li>hasn't subscribed yet</li>
<li>has seen the popup more than three times</li>
<li>has already seen the popup today</li>
</ul>
<p>Let's implement that logic:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> isToday = (date: <span class="hljs-built_in">Date</span>): <span class="hljs-function"><span class="hljs-params">boolean</span> =&gt;</span> {
  <span class="hljs-keyword">const</span> today = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
  <span class="hljs-keyword">return</span> (
    date.getDate() === today.getDate() &amp;&amp;
    date.getMonth() === today.getMonth() &amp;&amp;
    date.getFullYear() === today.getFullYear()
  );
};

<span class="hljs-keyword">const</span> config = {
  timeoutInMs: <span class="hljs-number">3000</span>,
  maxSeenCount: <span class="hljs-number">5</span>,
  scrollYInPxThreshold: <span class="hljs-number">300</span>,
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> usePolitePopup = <span class="hljs-function">() =&gt;</span> {
  ...
  watch(
    [readTimeElapsed, scrolledContent],
    <span class="hljs-function">(<span class="hljs-params">[newReadTimeElapsed, newScrolledContent]</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (storedData.value.status === <span class="hljs-string">'subscribed'</span>) {
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">if</span> (storedData.value.seenCount &gt;= config.maxSeenCount) {
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">if</span> (
        storedData.value.lastSeenAt &amp;&amp;
        isToday(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(storedData.value.lastSeenAt))
      ) {
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">if</span> (newReadTimeElapsed &amp;&amp; newScrolledContent) {
        visible.value = <span class="hljs-literal">true</span>;
        storedData.value.seenCount += <span class="hljs-number">1</span>;
        storedData.value.lastSeenAt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getTime();
      }
    }
  };
  ...

  <span class="hljs-keyword">return</span> {
    ...
  }
}
</code></pre>
<p>We are done!</p>
<p>You will probably also have seen this polite popup on this page if you read it on my <a target="_blank" href="https://mokkapps.de/building-a-polite-newsletter-popup-with-nuxt-3">portfolio website</a>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In my opinion, polite popups are the best way to convert visitors to my newsletter. This way, I can ensure that they are interested in my content and do not get annoyed by modals.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Create an RSS Feed With Nuxt 3 and Nuxt Content v2]]></title><description><![CDATA[My portfolio website is built with Nuxt 3 and Nuxt Content v2. An RSS feed with my latest five blog posts is available here. In this article, you'll learn how to add an RSS feed to your Nuxt website.
Setup
First, let's create a new Nuxt 3 project. As...]]></description><link>https://blog.mokkapps.de/create-an-rss-feed-with-nuxt-3-and-nuxt-content-v2</link><guid isPermaLink="true">https://blog.mokkapps.de/create-an-rss-feed-with-nuxt-3-and-nuxt-content-v2</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[Nuxt]]></category><category><![CDATA[Nuxt.js]]></category><category><![CDATA[rss]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Mon, 15 Aug 2022 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1660630892158/RSumbLp9MR.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My <a target="_blank" href="https:://mokkapps.de">portfolio website</a> is built with <a target="_blank" href="https://v3.nuxtjs.org/">Nuxt 3</a> and <a target="_blank" href="https://content.nuxtjs.org/">Nuxt Content v2</a>. An RSS feed with my latest five blog posts is available <a target="_blank" href="https://mokkkapps.de/rss.xml">here</a>. In this article, you'll learn how to add an RSS feed to your Nuxt website.</p>
<h2 id="heading-setup">Setup</h2>
<p>First, let's <a target="_blank" href="https://v3.nuxtjs.org/getting-started/quick-start">create a new Nuxt 3 project</a>. As the next step, we need to <a target="_blank" href="https://content.nuxtjs.org/get-started">add the Nuxt Content v2 module</a> to our application.</p>
<p>Finally, let's add some content that will be included in the RSS feed:</p>
<pre><code>├── content
<span class="hljs-operator">|</span>  └── blog
<span class="hljs-operator">|</span>  └── blog
<span class="hljs-operator">|</span>  <span class="hljs-operator">|</span>   ├── article<span class="hljs-number">-1</span>.md
<span class="hljs-operator">|</span>  <span class="hljs-operator">|</span>   ├── article<span class="hljs-number">-2</span>.md
<span class="hljs-operator">|</span>  <span class="hljs-operator">|</span>   ├── article<span class="hljs-number">-3</span>.md
<span class="hljs-operator">|</span>  <span class="hljs-operator">|</span>   ├── article<span class="hljs-number">-4</span>.md
<span class="hljs-operator">|</span>  <span class="hljs-operator">|</span>   ├── article<span class="hljs-number">-5</span>.md
</code></pre><p>Each <code>.md</code> file has this simple structure:</p>
<pre><code class="lang-md">---
title: 'Article 1'
description: 'Article 1 description'
<span class="hljs-section">date: '2022-01-01'
---</span>

Article 5 Content
</code></pre>
<p>The source code for this demo is available at <a target="_blank" href="https://github.com/Mokkapps/rss-feed-nuxt-3-and-nuxt-content-v2">GitHub</a> and in this StackBlitz playground:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://stackblitz.com/edit/rss-feed-nuxt-3-and-nuxt-content-v2?ctl=1&amp;embed=1&amp;file=README.md">https://stackblitz.com/edit/rss-feed-nuxt-3-and-nuxt-content-v2?ctl=1&amp;embed=1&amp;file=README.md</a></div>
<h2 id="heading-add-server-route">Add Server Route</h2>
<p>We will be utilizing the <a target="_blank" href="https://v3.nuxtjs.org/guide/features/server-routes">server routes</a> available within Nuxt, and to do so, we'll need to create the <code>server/</code> directory within our app root directly.</p>
<p>Once this is done, we create a <code>routes/</code> directory inside this and add a <code>rss.xml.ts</code> file. It will translate to <code>/rss.xml</code>:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineEventHandler(<span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-keyword">const</span> feedString = <span class="hljs-string">''</span>
  event.res.setHeader(<span class="hljs-string">'content-type'</span>, <span class="hljs-string">'text/xml'</span>)
  event.res.end(feedString)
})
</code></pre>
<p>The next step is to query our blog posts:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { serverQueryContent } <span class="hljs-keyword">from</span> <span class="hljs-string">'#content/server'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineEventHandler(<span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-keyword">const</span> docs = <span class="hljs-keyword">await</span> serverQueryContent(event).sort({ date: <span class="hljs-number">-1</span> }).where({ _partial: <span class="hljs-literal">false</span> }).find()
  <span class="hljs-keyword">const</span> blogPosts = docs.filter(<span class="hljs-function">(<span class="hljs-params">doc</span>) =&gt;</span> doc?._path?.includes(<span class="hljs-string">'/blog'</span>))

  <span class="hljs-keyword">const</span> feedString = <span class="hljs-string">''</span>
  event.res.setHeader(<span class="hljs-string">'content-type'</span>, <span class="hljs-string">'text/xml'</span>)
  event.res.end(feedString)
})
</code></pre>
<p>Now let's add the <a target="_blank" href="https://www.npmjs.com/package/rss">rss</a> library to generate the RSS XML string based on our content:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { serverQueryContent } <span class="hljs-keyword">from</span> <span class="hljs-string">'#content/server'</span>
<span class="hljs-keyword">import</span> RSS <span class="hljs-keyword">from</span> <span class="hljs-string">'rss'</span>

<span class="hljs-keyword">const</span> feed = <span class="hljs-keyword">new</span> RSS({
  title: <span class="hljs-string">'Michael Hoffmann'</span>,
  site_url: <span class="hljs-string">'https://mokkapps.de'</span>,
  feed_url: <span class="hljs-string">`https://mokkapps.de/rss.xml`</span>,
})

<span class="hljs-keyword">const</span> docs = <span class="hljs-keyword">await</span> serverQueryContent(event).sort({ date: <span class="hljs-number">-1</span> }).where({ _partial: <span class="hljs-literal">false</span> }).find()
<span class="hljs-keyword">const</span> blogPosts = docs.filter(<span class="hljs-function">(<span class="hljs-params">doc</span>) =&gt;</span> doc?._path?.includes(<span class="hljs-string">'/blog'</span>))

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> doc <span class="hljs-keyword">of</span> blogPosts) {
  feed.item({
    title: doc.title ?? <span class="hljs-string">'-'</span>,
    url: <span class="hljs-string">`https://mokkapps.de<span class="hljs-subst">${doc._path}</span>`</span>,
    date: doc.date,
    description: doc.description,
  })
}

<span class="hljs-keyword">const</span> feedString = feed.xml({ indent: <span class="hljs-literal">true</span> })
event.res.setHeader(<span class="hljs-string">'content-type'</span>, <span class="hljs-string">'text/xml'</span>)
event.res.end(feedString)
</code></pre>
<p>When using <code>nuxt generate</code>, you may want to pre-render the feed since the server route won't be able to run on a static hosting.</p>
<p>We can do this by using the <code>nitro.prerender</code> option in <code>nuxt.config</code>:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { defineNuxtConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'nuxt'</span>

<span class="hljs-comment">// https://v3.nuxtjs.org/api/configuration/nuxt.config</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineNuxtConfig({
  modules: [<span class="hljs-string">'@nuxt/content'</span>],
  nitro: {
    prerender: {
      routes: [<span class="hljs-string">'/rss.xml'</span>],
    },
  },
  content: {
    <span class="hljs-comment">// https://content.nuxtjs.org/api/configuration</span>
  },
})
</code></pre>
<p>If we now navigate to <code>/rss.xml</code>, we get our generated RSS feed:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">rss</span> <span class="hljs-attr">xmlns:dc</span>=<span class="hljs-string">"http://purl.org/dc/elements/1.1/"</span>
  <span class="hljs-attr">xmlns:content</span>=<span class="hljs-string">"http://purl.org/rss/1.0/modules/content/"</span>
  <span class="hljs-attr">xmlns:atom</span>=<span class="hljs-string">"http://www.w3.org/2005/Atom"</span> <span class="hljs-attr">version</span>=<span class="hljs-string">"2.0"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">channel</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>
      &lt;![CDATA[ Michael Hoffmann ]]&gt;
    <span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>
      &lt;![CDATA[ Michael Hoffmann ]]&gt;
    <span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">link</span>&gt;</span>https://mokkapps.de<span class="hljs-tag">&lt;/<span class="hljs-name">link</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">generator</span>&gt;</span>RSS for Node<span class="hljs-tag">&lt;/<span class="hljs-name">generator</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">lastBuildDate</span>&gt;</span>Sun, 14 Aug 2022 18:14:16 GMT<span class="hljs-tag">&lt;/<span class="hljs-name">lastBuildDate</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">atom:link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"https://mokkapps.de/rss.xml"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"self"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"application/rss+xml"</span>/&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">item</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>
        &lt;![CDATA[ Article 5 ]]&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>
        &lt;![CDATA[ Article 5 description ]]&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">link</span>&gt;</span>https://mokkapps.de/blog/article-5<span class="hljs-tag">&lt;/<span class="hljs-name">link</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">guid</span> <span class="hljs-attr">isPermaLink</span>=<span class="hljs-string">"true"</span>&gt;</span>https://mokkapps.de/blog/article-5<span class="hljs-tag">&lt;/<span class="hljs-name">guid</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">pubDate</span>&gt;</span>Thu, 05 May 2022 00:00:00 GMT<span class="hljs-tag">&lt;/<span class="hljs-name">pubDate</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">item</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">item</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>
        &lt;![CDATA[ Article 4 ]]&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>
        &lt;![CDATA[ Article 4 description ]]&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">link</span>&gt;</span>https://mokkapps.de/blog/article-4<span class="hljs-tag">&lt;/<span class="hljs-name">link</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">guid</span> <span class="hljs-attr">isPermaLink</span>=<span class="hljs-string">"true"</span>&gt;</span>https://mokkapps.de/blog/article-4<span class="hljs-tag">&lt;/<span class="hljs-name">guid</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">pubDate</span>&gt;</span>Mon, 04 Apr 2022 00:00:00 GMT<span class="hljs-tag">&lt;/<span class="hljs-name">pubDate</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">item</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">item</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>
        &lt;![CDATA[ Article 3 ]]&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>
        &lt;![CDATA[ Article 3 description ]]&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">link</span>&gt;</span>https://mokkapps.de/blog/article-3<span class="hljs-tag">&lt;/<span class="hljs-name">link</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">guid</span> <span class="hljs-attr">isPermaLink</span>=<span class="hljs-string">"true"</span>&gt;</span>https://mokkapps.de/blog/article-3<span class="hljs-tag">&lt;/<span class="hljs-name">guid</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">pubDate</span>&gt;</span>Thu, 03 Mar 2022 00:00:00 GMT<span class="hljs-tag">&lt;/<span class="hljs-name">pubDate</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">item</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">item</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>
        &lt;![CDATA[ Article 2 ]]&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>
        &lt;![CDATA[ Article 2 description ]]&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">link</span>&gt;</span>https://mokkapps.de/blog/article-2<span class="hljs-tag">&lt;/<span class="hljs-name">link</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">guid</span> <span class="hljs-attr">isPermaLink</span>=<span class="hljs-string">"true"</span>&gt;</span>https://mokkapps.de/blog/article-2<span class="hljs-tag">&lt;/<span class="hljs-name">guid</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">pubDate</span>&gt;</span>Wed, 02 Feb 2022 00:00:00 GMT<span class="hljs-tag">&lt;/<span class="hljs-name">pubDate</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">item</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">item</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>
        &lt;![CDATA[ Article 1 ]]&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>
        &lt;![CDATA[ Article 1 description ]]&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">link</span>&gt;</span>https://mokkapps.de/blog/article-1<span class="hljs-tag">&lt;/<span class="hljs-name">link</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">guid</span> <span class="hljs-attr">isPermaLink</span>=<span class="hljs-string">"true"</span>&gt;</span>https://mokkapps.de/blog/article-1<span class="hljs-tag">&lt;/<span class="hljs-name">guid</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">pubDate</span>&gt;</span>Sat, 01 Jan 2022 00:00:00 GMT<span class="hljs-tag">&lt;/<span class="hljs-name">pubDate</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">item</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">channel</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">rss</span>&gt;</span>
</code></pre>
<hr />
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[How to Create a Custom Code Block With Nuxt Content v2]]></title><description><![CDATA[title: How to Create a Custom Code Block With Nuxt Content v2
published: true
date: 
tags: nuxt, vue, nuxtjs, vuejs, webdev
canonical_url: https://www.mokkapps.de/blog/how-to-create-a-custom-code-block-with-nuxt-content-v2/
cover_image: https://dev-t...]]></description><link>https://blog.mokkapps.de/how-to-create-a-custom-code-block-with-nuxt-content-v2</link><guid isPermaLink="true">https://blog.mokkapps.de/how-to-create-a-custom-code-block-with-nuxt-content-v2</guid><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Wed, 15 Jun 2022 09:01:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1655288148633/KhNLNlkJQ.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p>title: How to Create a Custom Code Block With Nuxt Content v2
published: true
date: 
tags: nuxt, vue, nuxtjs, vuejs, webdev
canonical_url: https://www.mokkapps.de/blog/how-to-create-a-custom-code-block-with-nuxt-content-v2/</p>
<h2 id="heading-coverimage-httpsdev-to-uploadss3amazonawscomuploadsarticlesiy8xll86f72phzgata1sjpg">cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iy8xll86f72phzgata1s.jpg</h2>
<p>Code blocks are essential for blogs about software development. In this article, I want to show you how can define a custom code block component in <a target="_blank" href="https://content.nuxtjs.org/">Nuxt Content v2</a> with the following features:</p>
<ul>
<li>Custom styling for code blocks inside Markdown files</li>
<li>Show language name (if available)</li>
<li>Show file name (if available)</li>
<li>Show a “Copy Code” button</li>
</ul>
<h2 id="heading-nuxt-content-v2nuxt-content-v2">Nuxt Content v2<a class="post-section-overview" href="#nuxt-content-v2"></a></h2>
<p><a target="_blank" href="https://content.nuxtjs.org/">Nuxt Content v2</a> is a <a target="_blank" href="https://v3.nuxtjs.org/">Nuxt 3</a> module that reads local files from the <code>/content</code> directory in your project. It supports <code>.md</code>, <code>.yml</code>, <code>.csv</code> and <code>.json</code> files. Additionally, it’s possible to use Vue components in Markdown with the <a target="_blank" href="https://content.nuxtjs.org/guide/writing/mdc">MDC Syntax</a>.</p>
<h2 id="heading-setup-nuxt-appsetup-nuxt-app">Setup Nuxt App<a class="post-section-overview" href="#setup-nuxt-app"></a></h2>
<p>First, let’s start a new Nuxt Content project with:</p>
<pre><code>npx nuxi init nuxt<span class="hljs-operator">-</span>custom<span class="hljs-operator">-</span>code<span class="hljs-operator">-</span>blocks <span class="hljs-operator">-</span>t content
</code></pre><p>Then we need to install the dependencies in the <code>nuxt-custom-code-blocks</code> folder:</p>
<pre><code><span class="hljs-attribute">yarn</span> install
</code></pre><p>Now we can start the Nuxt content app in development mode:</p>
<pre><code><span class="hljs-attribute">yarn</span> dev
</code></pre><p>A browser window should automatically open for <code>http://localhost:3000</code>. Alternatively, you can start playing with Nuxt Content in your browser using <a target="_blank" href="https://stackblitz.com/github/nuxt/starter/tree/content">StackBlitz</a> or <a target="_blank" href="https://codesandbox.io/s/github/nuxt/starter/tree/content">CodeSandbox</a>.</p>
<p>The following <a target="_blank" href="https://stackblitz.com/edit/nuxt-content-v2-custom-code-blocks">StackBlitz sandbox</a> demonstrates the application we create in this article:</p>
<p>{% embed https://stackblitz.com/edit/nuxt-content-v2-custom-code-blocks?embed=1&amp;ctl=1 %}</p>
<h2 id="heading-custom-prose-componentcustom-prose-component">Custom Prose Component<a class="post-section-overview" href="#custom-prose-component"></a></h2>
<p><a target="_blank" href="https://content.nuxtjs.org/guide/writing/markdown#prose">Prose</a> represents the HTML tags output from the Markdown syntax in Nuxt Content. Nuxt Content provides a Vue component for each HTML tag like links, title levels, etc.</p>
<p>It’s possible to override these Vue components, which is precisely what we’ll do to create a custom code block component.</p>
<p>To customize a Prose component, we have to perform these steps:</p>
<ul>
<li>Check out the original component sources.</li>
<li>Use the same props.</li>
<li>Name it the same in our <code>components/content/</code> directory.</li>
</ul>
<p>In our example, we want to override <a target="_blank" href="https://github.com/nuxt/content/blob/main/src/runtime/components/Prose/ProseCode.vue">ProseCode</a>, which is Nuxt Content’s default Vue component to render code blocks in Markdown files.</p>
<p>This component accepts the following props:</p>
<ul>
<li><code>code</code>: the provided code as a string</li>
<li><code>language</code>: the provided language name</li>
<li><code>filename</code>: the provided filename</li>
<li><code>highlights</code>: a list of highlighted line numbers</li>
</ul>
<p>Let’s take a look at how we can set these values in a Markdown file:</p>
<pre><code>js [src<span class="hljs-operator">/</span>index.js] {<span class="hljs-number">1</span>, <span class="hljs-number">2</span><span class="hljs-number">-3</span>}
  const a <span class="hljs-operator">=</span> <span class="hljs-number">4</span>;
  const b <span class="hljs-operator">=</span> a <span class="hljs-operator">+</span> <span class="hljs-number">3</span>;
  const c <span class="hljs-operator">=</span> a <span class="hljs-operator">*</span> b;
</code></pre><p>In the above example:</p>
<ul>
<li><code>js</code> is the value passed to the <code>language</code> prop</li>
<li><code>src/index.js</code> is the value passed to the <code>filename</code> prop</li>
<li><code>[1, 2, 3]</code> is the value passed to the <code>highlights</code> prop</li>
</ul>
<p>To override the component, we create <code>ProseCode.vue</code> in the <code>components/content</code> directory and use the exact same props that are defined in the default component:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;slot /&gt;
&lt;/template&gt;

&lt;script setup lang="ts"&gt;
const props = withDefaults(
  defineProps&lt;{
    code?: string;
    language?: string | null;
    filename?: string | null;
    highlights?: Array&lt;number&gt;;
  }&gt;(),
  { code: '', language: null, filename: null, highlights: [] }
);
&lt;/script&gt;
</code></pre>
<p>Now we can customize this component however we want.</p>
<h2 id="heading-style-containerstyle-container">Style Container<a class="post-section-overview" href="#style-container"></a></h2>
<p>First, we want to style the container that includes the code. Therefore, we wrap the <code>&lt;slot /&gt;</code> in a <code>div</code> and style it:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="container"&gt;
    &lt;slot /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;style scoped&gt;
.container {
  background: #1e1e1e;
  position: relative;
  margin-top: 1rem;
  margin-bottom: 1rem;
  overflow: hidden;
  border-radius: 0.5rem;
}
&lt;/style&gt;
</code></pre>
<p>Let’s take a look at our custom code block:</p>
<p><a target="_blank" href="/static/4852784fbb314d749fbf1715ed2acfc8/dea13/code-container-styled.png">![Styled Code Block Container](https://www.mokkapps.de/static/4852784fbb314d749fbf1715ed2acfc8/dea13/code-container-styled.png "Styled Code Block Container")</a>
<em>Styled Code Block Container</em></p>
<h2 id="heading-show-languageshow-language">Show Language<a class="post-section-overview" href="#show-language"></a></h2>
<p>Next, we want to show the name of the language on the top right, if it is available.</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="container"&gt;
    &lt;span v-if="languageText" :style="{ background: languageBackground, color: languageColor }" class="language-text" &gt; {{ languageText }} &lt;/span&gt; &lt;slot /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script setup lang="ts"&gt;
const props = withDefaults(
  defineProps&lt;{
    code?: string;
    language?: string | null;
    filename?: string | null;
    highlights?: Array&lt;number&gt;;
  }&gt;(),
  { code: '', language: null, filename: null, highlights: [] }
);

const languageMap: Record&lt;
  string,
  { text: string; color: string; background: string }
&gt; = {
  vue: {
    text: 'vue',
    background: '#42b883',
    color: 'white',
  },
  js: {
    text: 'js',
    background: '#f7df1e',
    color: 'black',
  },
};

const languageText = computed(() =&gt;
  props.language ? languageMap[props.language]?.text : null
);
const languageBackground = computed(() =&gt;
  props.language ? languageMap[props.language]?.background : null
);
const languageColor = computed(() =&gt;
  props.language ? languageMap[props.language]?.color : null
);
&lt;/script&gt;

&lt;style scoped&gt;
.container {
  background: #1e1e1e;
  padding-top: 1em;
}

.language-text {
  position: absolute;
  top: 0;
  right: 1em;
  padding: 0.25em 0.5em;
  font-size: 14px;
  text-transform: uppercase;
  border-bottom-right-radius: 0.25em;
  border-bottom-left-radius: 0.25em;
}
&lt;/style&gt;
</code></pre>
<p>We define a map called <code>languageMap</code> that contains the displayed text, the CSS background, and text color for each programming language. We style the <code>span</code> tag that renders the language inside our template based on this map and the provided <code>language</code> prop:</p>
<p><a target="_blank" href="/static/4a85f1693cb2d82850c2619912fbcc87/c6bbc/code-block-with-language-name.png">![Code block with language name](https://www.mokkapps.de/static/4a85f1693cb2d82850c2619912fbcc87/1e043/code-block-with-language-name.png "Code block with language name")</a>
<em>Code block with language name</em></p>
<h2 id="heading-show-file-nameshow-file-name">Show File Name<a class="post-section-overview" href="#show-file-name"></a></h2>
<p>Next, we want to show the file’s name on the top left, if it is available:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="container"&gt;
    &lt;span v-if="filename" class="filename-text"&gt;
      {{ filename }}
    &lt;/span&gt;
    &lt;slot /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;style scoped&gt;
.filename-text {
  position: absolute;
  top: 0;
  left: 1em;
  padding: 0.25em 0.5em;
  color: white;
  font-size: 14px;
}
&lt;/style&gt;
</code></pre>
<p>The result looks like this:</p>
<p><a target="_blank" href="/static/7920feef78b2055b9e82ba8d60beae97/b04e4/code-block-with-filename.png">![Code block with file name](https://www.mokkapps.de/static/7920feef78b2055b9e82ba8d60beae97/1e043/code-block-with-filename.png "Code block with file name")</a>
<em>Code block with file name</em></p>
<h2 id="heading-add-copy-code-buttonadd-copy-code-button">Add Copy Code Button<a class="post-section-overview" href="#add-copy-code-button"></a></h2>
<p>Finally, we want to show a button that copies the code to the clipboard. Therefore, we use <a target="_blank" href="https://vueuse.org/core/useclipboard/#useclipboard=">the useClipboard composable from VueUse</a>:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="container"&gt;
    &lt;slot /&gt;
    &lt;div class="bottom-container"&gt;
      &lt;div class="copy-container"&gt;
        &lt;span class="copied-text" v-if="copied"&gt;Copied code!&lt;/span&gt;
        &lt;button @click="copy(code)"&gt;Copy Code&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script setup lang="ts"&gt;
import { useClipboard } from '@vueuse/core';

const { copy, copied, text } = useClipboard();
&lt;/script&gt;

&lt;style scoped&gt;
.bottom-container {
  display: flex;
  justify-content: flex-end;
}

.copy-container {
  display: flex;
}

.copied-text {
  margin-right: 1em;
}
&lt;/style&gt;
</code></pre>
<p>Let’s take a look at the final result with language &amp; file name, copy code button, and line highlighting:</p>
<p><a target="_blank" href="/static/86781e29ddc1a016b4aeb2b1b5630391/99f37/code-block-final.png">![Final custom code block](https://www.mokkapps.de/static/86781e29ddc1a016b4aeb2b1b5630391/1e043/code-block-final.png "Final custom code block")</a>
<em>Final custom code block</em></p>
<h2 id="heading-conclusionconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>Custom code blocks are essential for my blog as my blog posts contain a lot of code snippets. Features like copy code or line highlighting provide excellent value to my readers, and it is straightforward to add such features by creating a custom code block component in Nuxt Content v2.</p>
<p>The source code of this demo is available at <a target="_blank" href="https://github.com/Mokkapps/nuxt-content-v2-custom-code-blocks/tree/master">GitHub</a> or as <a target="_blank" href="https://stackblitz.com/edit/nuxt-content-v2-custom-code-blocks">StackBlitz sandbox</a>.</p>
<p>You can expect more <a target="_blank" href="https://v3.nuxtjs.org/">Nuxt 3</a> posts in the following months as I plan to blog about interesting topics that I discover while rewriting my portfolio website.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[How to Create a Custom Code Block With Nuxt Content v2]]></title><description><![CDATA[Code blocks are essential for blogs about software development. In this article, I want to show you how can define a custom code block component in Nuxt Content v2 with the following features:

Custom styling for code blocks inside Markdown files
Sho...]]></description><link>https://blog.mokkapps.de/how-to-create-a-custom-code-block-with-nuxt-content-v2-1</link><guid isPermaLink="true">https://blog.mokkapps.de/how-to-create-a-custom-code-block-with-nuxt-content-v2-1</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[Nuxt]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Wed, 15 Jun 2022 09:01:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1655288284927/yuP0eMBVZ.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Code blocks are essential for blogs about software development. In this article, I want to show you how can define a custom code block component in <a target="_blank" href="https://content.nuxtjs.org/">Nuxt Content v2</a> with the following features:</p>
<ul>
<li>Custom styling for code blocks inside Markdown files</li>
<li>Show language name (if available)</li>
<li>Show file name (if available)</li>
<li>Show a “Copy Code” button</li>
</ul>
<h2 id="heading-nuxt-content-v2nuxt-content-v2">Nuxt Content v2<a class="post-section-overview" href="#nuxt-content-v2"></a></h2>
<p><a target="_blank" href="https://content.nuxtjs.org/">Nuxt Content v2</a> is a <a target="_blank" href="https://v3.nuxtjs.org/">Nuxt 3</a> module that reads local files from the <code>/content</code> directory in your project. It supports <code>.md</code>, <code>.yml</code>, <code>.csv</code> and <code>.json</code> files. Additionally, it’s possible to use Vue components in Markdown with the <a target="_blank" href="https://content.nuxtjs.org/guide/writing/mdc">MDC Syntax</a>.</p>
<h2 id="heading-setup-nuxt-appsetup-nuxt-app">Setup Nuxt App<a class="post-section-overview" href="#setup-nuxt-app"></a></h2>
<p>First, let’s start a new Nuxt Content project with:</p>
<pre><code>npx nuxi init nuxt<span class="hljs-operator">-</span>custom<span class="hljs-operator">-</span>code<span class="hljs-operator">-</span>blocks <span class="hljs-operator">-</span>t content
</code></pre><p>Then we need to install the dependencies in the <code>nuxt-custom-code-blocks</code> folder:</p>
<pre><code><span class="hljs-attribute">yarn</span> install
</code></pre><p>Now we can start the Nuxt content app in development mode:</p>
<pre><code><span class="hljs-attribute">yarn</span> dev
</code></pre><p>A browser window should automatically open for <code>http://localhost:3000</code>. Alternatively, you can start playing with Nuxt Content in your browser using <a target="_blank" href="https://stackblitz.com/github/nuxt/starter/tree/content">StackBlitz</a> or <a target="_blank" href="https://codesandbox.io/s/github/nuxt/starter/tree/content">CodeSandbox</a>.</p>
<p>The following <a target="_blank" href="https://stackblitz.com/edit/nuxt-content-v2-custom-code-blocks">StackBlitz sandbox</a> demonstrates the application we create in this article:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://stackblitz.com/edit/nuxt-content-v2-custom-code-blocks?embed=1&amp;ctl=1">https://stackblitz.com/edit/nuxt-content-v2-custom-code-blocks?embed=1&amp;ctl=1</a></div>
<h2 id="heading-custom-prose-componentcustom-prose-component">Custom Prose Component<a class="post-section-overview" href="#custom-prose-component"></a></h2>
<p><a target="_blank" href="https://content.nuxtjs.org/guide/writing/markdown#prose">Prose</a> represents the HTML tags output from the Markdown syntax in Nuxt Content. Nuxt Content provides a Vue component for each HTML tag like links, title levels, etc.</p>
<p>It’s possible to override these Vue components, which is precisely what we’ll do to create a custom code block component.</p>
<p>To customize a Prose component, we have to perform these steps:</p>
<ul>
<li>Check out the original component sources.</li>
<li>Use the same props.</li>
<li>Name it the same in our <code>components/content/</code> directory.</li>
</ul>
<p>In our example, we want to override <a target="_blank" href="https://github.com/nuxt/content/blob/main/src/runtime/components/Prose/ProseCode.vue">ProseCode</a>, which is Nuxt Content’s default Vue component to render code blocks in Markdown files.</p>
<p>This component accepts the following props:</p>
<ul>
<li><code>code</code>: the provided code as a string</li>
<li><code>language</code>: the provided language name</li>
<li><code>filename</code>: the provided filename</li>
<li><code>highlights</code>: a list of highlighted line numbers</li>
</ul>
<p>Let’s take a look at how we can set these values in a Markdown file:</p>
<pre><code>js [src<span class="hljs-operator">/</span>index.js] {<span class="hljs-number">1</span>, <span class="hljs-number">2</span><span class="hljs-number">-3</span>}
  const a <span class="hljs-operator">=</span> <span class="hljs-number">4</span>;
  const b <span class="hljs-operator">=</span> a <span class="hljs-operator">+</span> <span class="hljs-number">3</span>;
  const c <span class="hljs-operator">=</span> a <span class="hljs-operator">*</span> b;
</code></pre><p>In the above example:</p>
<ul>
<li><code>js</code> is the value passed to the <code>language</code> prop</li>
<li><code>src/index.js</code> is the value passed to the <code>filename</code> prop</li>
<li><code>[1, 2, 3]</code> is the value passed to the <code>highlights</code> prop</li>
</ul>
<p>To override the component, we create <code>ProseCode.vue</code> in the <code>components/content</code> directory and use the exact same props that are defined in the default component:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;slot /&gt;
&lt;/template&gt;

&lt;script setup lang="ts"&gt;
const props = withDefaults(
  defineProps&lt;{
    code?: string;
    language?: string | null;
    filename?: string | null;
    highlights?: Array&lt;number&gt;;
  }&gt;(),
  { code: '', language: null, filename: null, highlights: [] }
);
&lt;/script&gt;
</code></pre>
<p>Now we can customize this component however we want.</p>
<h2 id="heading-style-containerstyle-container">Style Container<a class="post-section-overview" href="#style-container"></a></h2>
<p>First, we want to style the container that includes the code. Therefore, we wrap the <code>&lt;slot /&gt;</code> in a <code>div</code> and style it:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="container"&gt;
    &lt;slot /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;style scoped&gt;
.container {
  background: #1e1e1e;
  position: relative;
  margin-top: 1rem;
  margin-bottom: 1rem;
  overflow: hidden;
  border-radius: 0.5rem;
}
&lt;/style&gt;
</code></pre>
<p>Let’s take a look at our custom code block:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655288376812/eblZYEtLQ.png" alt="code-container-styled.png" /></p>
<h2 id="heading-show-languageshow-language">Show Language<a class="post-section-overview" href="#show-language"></a></h2>
<p>Next, we want to show the name of the language on the top right, if it is available.</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="container"&gt;
    &lt;span v-if="languageText" :style="{ background: languageBackground, color: languageColor }" class="language-text" &gt; {{ languageText }} &lt;/span&gt; &lt;slot /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script setup lang="ts"&gt;
const props = withDefaults(
  defineProps&lt;{
    code?: string;
    language?: string | null;
    filename?: string | null;
    highlights?: Array&lt;number&gt;;
  }&gt;(),
  { code: '', language: null, filename: null, highlights: [] }
);

const languageMap: Record&lt;
  string,
  { text: string; color: string; background: string }
&gt; = {
  vue: {
    text: 'vue',
    background: '#42b883',
    color: 'white',
  },
  js: {
    text: 'js',
    background: '#f7df1e',
    color: 'black',
  },
};

const languageText = computed(() =&gt;
  props.language ? languageMap[props.language]?.text : null
);
const languageBackground = computed(() =&gt;
  props.language ? languageMap[props.language]?.background : null
);
const languageColor = computed(() =&gt;
  props.language ? languageMap[props.language]?.color : null
);
&lt;/script&gt;

&lt;style scoped&gt;
.container {
  background: #1e1e1e;
  padding-top: 1em;
}

.language-text {
  position: absolute;
  top: 0;
  right: 1em;
  padding: 0.25em 0.5em;
  font-size: 14px;
  text-transform: uppercase;
  border-bottom-right-radius: 0.25em;
  border-bottom-left-radius: 0.25em;
}
&lt;/style&gt;
</code></pre>
<p>We define a map called <code>languageMap</code> that contains the displayed text, the CSS background, and text color for each programming language. We style the <code>span</code> tag that renders the language inside our template based on this map and the provided <code>language</code> prop:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655288393918/zW6rYwN4c.png" alt="code-block-with-language-name.png" /></p>
<h2 id="heading-show-file-nameshow-file-name">Show File Name<a class="post-section-overview" href="#show-file-name"></a></h2>
<p>Next, we want to show the file’s name on the top left, if it is available:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="container"&gt;
    &lt;span v-if="filename" class="filename-text"&gt;
      {{ filename }}
    &lt;/span&gt;
    &lt;slot /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;style scoped&gt;
.filename-text {
  position: absolute;
  top: 0;
  left: 1em;
  padding: 0.25em 0.5em;
  color: white;
  font-size: 14px;
}
&lt;/style&gt;
</code></pre>
<p>The result looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655288403812/lAXCPcXFF.png" alt="code-block-with-filename.png" /></p>
<h2 id="heading-add-copy-code-buttonadd-copy-code-button">Add Copy Code Button<a class="post-section-overview" href="#add-copy-code-button"></a></h2>
<p>Finally, we want to show a button that copies the code to the clipboard. Therefore, we use <a target="_blank" href="https://vueuse.org/core/useclipboard/#useclipboard=">the useClipboard composable from VueUse</a>:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="container"&gt;
    &lt;slot /&gt;
    &lt;div class="bottom-container"&gt;
      &lt;div class="copy-container"&gt;
        &lt;span class="copied-text" v-if="copied"&gt;Copied code!&lt;/span&gt;
        &lt;button @click="copy(code)"&gt;Copy Code&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script setup lang="ts"&gt;
import { useClipboard } from '@vueuse/core';

const { copy, copied, text } = useClipboard();
&lt;/script&gt;

&lt;style scoped&gt;
.bottom-container {
  display: flex;
  justify-content: flex-end;
}

.copy-container {
  display: flex;
}

.copied-text {
  margin-right: 1em;
}
&lt;/style&gt;
</code></pre>
<p>Let’s take a look at the final result with language &amp; file name, copy code button, and line highlighting:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655288415163/AWCg4Cs2r.png" alt="code-block-final.png" /></p>
<h2 id="heading-conclusionconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>Custom code blocks are essential for my blog as my blog posts contain a lot of code snippets. Features like copy code or line highlighting provide excellent value to my readers, and it is straightforward to add such features by creating a custom code block component in Nuxt Content v2.</p>
<p>The source code of this demo is available at <a target="_blank" href="https://github.com/Mokkapps/nuxt-content-v2-custom-code-blocks/tree/master">GitHub</a> or as <a target="_blank" href="https://stackblitz.com/edit/nuxt-content-v2-custom-code-blocks">StackBlitz sandbox</a>.</p>
<p>You can expect more <a target="_blank" href="https://v3.nuxtjs.org/">Nuxt 3</a> posts in the following months as I plan to blog about interesting topics that I discover while rewriting my portfolio website.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Create a Blog With Nuxt Content v2]]></title><description><![CDATA[I prefer simple Markdown files as the content source for my blog posts. In this article, I want to show you how can set up a simple blog using Nuxt Content v2.
Nuxt Content v2
Nuxt Content v2 is a Nuxt 3 module that reads local files from the /conten...]]></description><link>https://blog.mokkapps.de/create-a-blog-with-nuxt-content-v2</link><guid isPermaLink="true">https://blog.mokkapps.de/create-a-blog-with-nuxt-content-v2</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[vue]]></category><category><![CDATA[Nuxt]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Blogging]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Thu, 02 Jun 2022 15:15:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1654183132965/OUWaQpD_Q.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I prefer simple Markdown files as the content source for my blog posts. In this article, I want to show you how can set up a simple blog using <a target="_blank" href="https://content.nuxtjs.org/">Nuxt Content v2</a>.</p>
<h2 id="heading-nuxt-content-v2">Nuxt Content v2</h2>
<p><a target="_blank" href="https://content.nuxtjs.org/">Nuxt Content v2</a> is a <a target="_blank" href="https://v3.nuxtjs.org/">Nuxt 3</a> module that reads local files from the <code>/content</code> directory in your project. It supports <code>.md</code>, <code>.yml</code>, <code>.csv</code> and <code>.json</code> files. Additionally, it’s possible to use Vue components in Markdown with the <a target="_blank" href="https://content.nuxtjs.org/guide/writing/mdc">MDC Syntax</a>.</p>
<h2 id="heading-setup-nuxt-app">Setup Nuxt App</h2>
<p>First, let’s start a new Nuxt Content project with:</p>
<pre><code class="lang-bash">npx nuxi init nuxt-demo-blog -t content
</code></pre>
<p>Then we need to install the dependencies in the <code>nuxt-demo-blog</code> folder:</p>
<pre><code class="lang-bash">yarn install
</code></pre>
<p>Now we can start the Nuxt content app in development mode:</p>
<pre><code class="lang-bash">yarn dev
</code></pre>
<p>A browser window should automatically open for <code>http://localhost:3000</code>. Alternatively, you can start playing with Nuxt Content in your browser using <a target="_blank" href="https://stackblitz.com/github/nuxt/starter/tree/content">StackBlitz</a> or <a target="_blank" href="https://codesandbox.io/s/github/nuxt/starter/tree/content">CodeSandbox</a>.</p>
<p>The following StackBlitz sandbox demonstrates the simple blog application we create in this article:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://stackblitz.com/edit/nuxt-content-v2-blog-demo?embed=1">https://stackblitz.com/edit/nuxt-content-v2-blog-demo?embed=1</a></div>
<h2 id="heading-blog-content-structure">Blog Content Structure</h2>
<p>Our demo blog will have this structure inside the <code>/content</code> directory:</p>
<pre><code>├── blog
│ ├── _index.md
│ ├── a<span class="hljs-operator">-</span>great<span class="hljs-operator">-</span>article
│ └── cover.jpg
│ │ └── index.md
│ └── another<span class="hljs-operator">-</span>great<span class="hljs-operator">-</span>article
│ └── cover.jpg
│ └── index.md
</code></pre><p><code>blog/_index.md</code> is a <a target="_blank" href="https://content.nuxtjs.org/guide/writing/content-directory#partials">Partial</a> content that will show a list of all available blog posts.</p>
<p>Each blog post has its directory, including an <code>index.md</code> and a <code>cover.jpg</code> file.</p>
<p>The <code>index.md</code> files include <a target="_blank" href="https://content.nuxtjs.org/guide/writing/markdown#front-matter">Front-matter</a> at the top of the file to provide meta-data to pages, like title, date, and the cover image URL:</p>
<pre><code><span class="hljs-meta">---</span>
<span class="hljs-attr">title:</span> <span class="hljs-string">A</span> <span class="hljs-string">Great</span> <span class="hljs-string">Article</span>
<span class="hljs-attr">date:</span> <span class="hljs-number">2018-05-11</span>
<span class="hljs-attr">cover:</span> <span class="hljs-string">/content/blog/a-great-article/cover.jpg</span>
<span class="hljs-meta">---</span>

<span class="hljs-string">This</span> <span class="hljs-string">is</span> <span class="hljs-string">a</span> <span class="hljs-string">great</span> <span class="hljs-string">article</span> <span class="hljs-string">body!</span>
</code></pre><h2 id="heading-simple-navigation">Simple Navigation</h2>
<p>First, we need simple navigation in our application to be able to navigate to our blog page.</p>
<p>Let’s start by adding a <a target="_blank" href="https://v3.nuxtjs.org/guide/directory-structure/layouts">default layout</a> in <code>layouts</code>:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;nav&gt;
      &lt;NuxtLink to="/" class="link"&gt;Home&lt;/NuxtLink&gt;
      &lt;NuxtLink to="/blog" class="link"&gt;Blog&lt;/NuxtLink&gt;
    &lt;/nav&gt;
    &lt;main&gt;
      &lt;slot /&gt;
    &lt;/main&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;style&gt;
.link {
  margin-right: 1rem;
}
&lt;/style&gt;
</code></pre>
<p>In our <code>app.vue</code> we need to wrap the NuxtPage component with the NuxtLayout component:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;NuxtLayout&gt;
      &lt;NuxtPage /&gt;
    &lt;/NuxtLayout&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>
<p>Finally, we create a <code>index.vue</code> in <code>pages</code> directory:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;h1&gt;Home&lt;/h1&gt;
&lt;/template&gt;
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654183231183/4lUitxf9_.png" alt="home.png" /></p>
<h2 id="heading-blog-list">Blog List</h2>
<p>Let’s look at how we can implement a list of all available blog posts.</p>
<p>First, we need to create a <code>BlogPosts.vue</code> Vue component in <code>components/content/</code> that queries and renders all available blog posts:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;h1&gt;Blog&lt;/h1&gt;
  &lt;ul&gt;
    &lt;li v-for="{ _path: slug, title } in blogPosts" :key="slug"&gt;
      &lt;NuxtLink :to="slug"&gt;{{ title }}&lt;/NuxtLink&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/template&gt;

&lt;script setup lang="ts"&gt;
const blogPosts = await queryContent('/blog')
  .sort({ date: -1 }) // show latest articles first
  .where({ _partial: false }) // exclude the Partial files
  .find();
&lt;/script&gt;
</code></pre>
<p>We use the <a target="_blank" href="https://content.nuxtjs.org/guide/displaying/querying#querying-content">queryContent function</a> from Nuxt to query a list of our blog posts.</p>
<p>Now we can reference this Vue component inside our <code>content/blog/_index.md</code> file:</p>
<pre><code><span class="hljs-meta">---</span>
<span class="hljs-attr">title:</span> <span class="hljs-string">Blog</span>
<span class="hljs-meta">---</span>

<span class="hljs-string">::blog-posts</span>
</code></pre><p>We can use any component in the <code>components/content/</code> directory or any component made available globally in your application in Markdown files.</p>
<p>If we now click on the “Blog” navigation link in our application, we can see a list of all available blog posts:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654183246194/2_kxtZxLo.png" alt="blog.png" /></p>
<p>I reported a <a target="_blank" href="https://github.com/nuxt/content/issues/1197">Nuxt content bug</a> that you need to reload some routes; otherwise, their content is not visible.</p>
<h2 id="heading-blog-post-page">Blog Post Page</h2>
<p>Finally, we need to create a <a target="_blank" href="https://v3.nuxtjs.org/guide/directory-structure/pages#dynamic-routes=">dynamic route</a> for the blog posts. Thus, we create a <code>[...slug].vue</code> file in <code>pages/blog</code>:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;ContentDoc
    :path="$route.params.slug ? `/blog/${$route.params.slug[0]}` : '/blog'"
  &gt;
    &lt;template #not-found&gt;
      &lt;h2&gt;Blog slug ({{ $route.params.slug }}) not found&lt;/h2&gt;
    &lt;/template&gt;
  &lt;/ContentDoc&gt;
&lt;/template&gt;
</code></pre>
<p>We use the current slug in the route parameters (<code>$route.params.slug</code>) to determine whether we want to render the blog post list or an individual blog post.</p>
<p>We can now see the content of the corresponding blog post:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654183263074/YEFj8uSTY.png" alt="blog-post.png" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>It’s effortless to create a Markdown file-based blog using <a target="_blank" href="https://content.nuxtjs.org/">Nuxt Content v2</a>. This article demonstrates the basic steps to set up such a blog.</p>
<p>You can expect more <a target="_blank" href="https://v3.nuxtjs.org/">Nuxt 3</a> posts in the following months as I plan to blog about interesting topics that I discover while rewriting my portfolio website.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Chrome Recorder: Record, Replay and Measure User Flows]]></title><description><![CDATA[Typically, a user needs to process multiple pages or steps to finish his journey, such as submitting an order or completing a registration. If we as developers need to develop one of the last pages of this user flow, we need to manually process all t...]]></description><link>https://blog.mokkapps.de/chrome-recorder-record-replay-and-measure-user-flows</link><guid isPermaLink="true">https://blog.mokkapps.de/chrome-recorder-record-replay-and-measure-user-flows</guid><category><![CDATA[Google Chrome]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Thu, 28 Apr 2022 09:01:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1651469835973/lSqICQS8e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Typically, a user needs to process multiple pages or steps to finish his journey, such as submitting an order or completing a registration. If we as developers need to develop one of the last pages of this user flow, we need to manually process all the previous pages/steps every time we refresh the page or need to restart the flow.</p>
<p>If multiple team members (also tester) have to do the same steps repeatedly, this costs a lot of time and, therefore, money. In previous projects, we often developed custom tools to proceed to certain pages/steps in the application automatically.</p>
<p>But now, the <a target="_blank" href="https://chrome.com/">Chrome browser</a> provides this functionality as a preview feature.</p>
<h2 id="heading-what-is-chrome-recorderwhat-is-chrome-recorder">What is Chrome Recorder?<a class="post-section-overview" href="#what-is-chrome-recorder"></a></h2>
<p><a target="_blank" href="https://developer.chrome.com/docs/devtools/recorder/">Chrome Recorder</a> is a preview feature in the Chrome browser designed to record, replay and measure user flows of an application.</p>
<p>You can start a recording, execute the steps you’d like to record in the app (such as typing or clicking), and export the recording as JSON file, <a target="_blank" href="https://pptr.dev/">Puppeteer</a> script or <a target="_blank" href="https://github.com/puppeteer/replay">@puppeteer/replay</a> script.</p>
<p>It’s then possible to replay the recorded user flow and measure the performance of the run.</p>
<h2 id="heading-open-recorderopen-recorder">Open Recorder<a class="post-section-overview" href="#open-recorder"></a></h2>
<p>To find the Recorder, you first open up the Chrome DevTools.</p>
<blockquote>
<p>Command+Option+C (Mac) or Control+Shift+C (Windows, Linux, ChromeOS).</p>
</blockquote>
<p>You can open the Recorder from the options menu:</p>
<p><a target="_blank" href="/static/12d7e2a17be57544804113670477bda4/f058b/chrome-recorder-open-more-tools.png"><img src="https://www.mokkapps.de/static/12d7e2a17be57544804113670477bda4/f058b/chrome-recorder-open-more-tools.png" alt="Open Chrome Recorder from options menu" /></a>
<em>Open Chrome Recorder from options menu</em></p>
<p>Alternatively, you can open it from <a target="_blank" href="https://developer.chrome.com/docs/devtools/command-menu/">Command Menu</a>:</p>
<p><img src="https://www.mokkapps.de/static/544cf48f47fd17c724de9c95e8526240/63a68/chrome-recorder-open-command-menu.png" alt="Open Chrome Recorder from Command Menu" />
<em>Open Chrome Recorder from Command Menu</em></p>
<h2 id="heading-recordrecord">Record<a class="post-section-overview" href="#record"></a></h2>
<p>I’ll be using <a target="_blank" href="https://github.com/Anivive/vue3-form-wizard">Vue 3 Form Wizard</a> to demonstrate the recording &amp; replaying of a simple user flow.</p>
<p><a target="_blank" href="https://vue3-form-wizard-demo.stackblitz.io/">The demo page</a> provides a simple wizard with multiple steps containing common input types like text &amp; select inputs.</p>
<p>Let’s start the recording:</p>
<p><a target="_blank" href="/static/7cfc795a2223e83e42049d0612c1ccf4/d61c2/chrome-recorder-start-recording.png"><img src="https://www.mokkapps.de/static/7cfc795a2223e83e42049d0612c1ccf4/1e043/chrome-recorder-start-recording.png" alt="Start Recording" /></a>
<em>Start Recording</em></p>
<p>The selector attribute textbox is optional. See <a target="_blank" href="https://developer.chrome.com/docs/devtools/recorder/#customize-selector">Customize the recording’s selector</a>.</p>
<p>Once you hit the record button, you can enter all data in the wizard. If you are done, hit the “End recording” button at the bottom of the recorder panel.</p>
<p><a target="_blank" href="/static/bcfe729a655dae272be43508059d14ba/d61c2/chrome-recorder-finished-recording.png"><img src="https://www.mokkapps.de/static/bcfe729a655dae272be43508059d14ba/1e043/chrome-recorder-finished-recording.png" alt="Recorded User Flow" /></a>
<em>Recorded User Flow</em></p>
<p>The following GIF visualizes this process:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651469832707/3wrtitnlX.gif" alt="Record user flow (GIF)" /></p>
<p>It’s also possible to manually edit the recorded steps. For example, you can manually change selectors:</p>
<p><a target="_blank" href="/static/f406dce319d2c28b701b231e039d89b4/ee455/chrome-recorder-selector.png"><img src="https://www.mokkapps.de/static/f406dce319d2c28b701b231e039d89b4/1e043/chrome-recorder-selector.png" alt="Change Selector" /></a>
<em>Change Selector</em></p>
<p>Additionally, you can manually add or remove steps:</p>
<p><a target="_blank" href="/static/c221cd1e4bbd1fdc5e7c037f2ab162f8/79e48/chrome-add-steps.png"><img src="https://www.mokkapps.de/static/c221cd1e4bbd1fdc5e7c037f2ab162f8/1e043/chrome-add-steps.png" alt="Add/Remove Steps" /></a>
<em>Add/Remove Steps</em></p>
<h2 id="heading-replayreplay">Replay<a class="post-section-overview" href="#replay"></a></h2>
<p>After recording a user flow, you can replay it by clicking on the “Replay” button.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651469834589/18NSnCHq2.gif" alt="Replay Recording (GIF)" /></p>
<p>When replaying a user flow recording, the Recorder waits until the element is visible or clickable in the viewport or tries to automatically scroll the element into the viewport before replaying the corresponding step.</p>
<p>It’s also possible to simulate a slow network connection in the “Replay” settings:</p>
<p><a target="_blank" href="/static/8668956b104fdea3e63f1cfa571298c8/fc2a6/chrome-recorder-replay-setting.png"><img src="https://www.mokkapps.de/static/8668956b104fdea3e63f1cfa571298c8/fc2a6/chrome-recorder-replay-setting.png" alt="Simulate Slow Network" /></a>
<em>Simulate Slow Network</em></p>
<h2 id="heading-measure-performancemeasure-performance">Measure performance<a class="post-section-overview" href="#measure-performance"></a></h2>
<p>You can also measure the performance of your recording by clicking the “Measure performance” button. This way, you can regularly measure the performance of critical user flows.</p>
<p><a target="_blank" href="/static/b408db5a0aba1d25bca88815a057d5fd/d61c2/chrome-recorder-performance.png"><img src="https://www.mokkapps.de/static/b408db5a0aba1d25bca88815a057d5fd/1e043/chrome-recorder-performance.png" alt="Performance Panel" /></a>
<em>Performance Panel</em></p>
<h2 id="heading-conclusionconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>The Chrome Recorder is valuable tool that will boost my productivity during development. It’s still a preview feature, but I think it will become a must-have tool for web developers.</p>
<p>I recommend reading the <a target="_blank" href="https://developer.chrome.com/docs/devtools/recorder/">official Chrome blog post</a>.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Dark Mode Switch With Tailwind CSS & Nuxt 3]]></title><description><![CDATA[I am currently rewriting my portfolio website with Nuxt 3 which is still in beta. In this article, I want to show you how I implemented a dark mode switch in Nuxt 3 using Tailwind CSS that I will use in my new portfolio website.
Create Nuxt 3 project...]]></description><link>https://blog.mokkapps.de/dark-mode-switch-with-tailwind-css-and-nuxt-3</link><guid isPermaLink="true">https://blog.mokkapps.de/dark-mode-switch-with-tailwind-css-and-nuxt-3</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[Nuxt]]></category><category><![CDATA[Tailwind CSS]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Mon, 21 Feb 2022 09:04:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1645434387194/0bnsWXIqV.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I am currently rewriting my <a target="_blank" href="https://github.com/mokkapps/website">portfolio website</a> with <a target="_blank" href="https://v3.nuxtjs.org/">Nuxt 3</a> which is still in beta. In this article, I want to show you how I implemented a dark mode switch in Nuxt 3 using <a target="_blank" href="https://tailwindcss.com/">Tailwind CSS</a> that I will use in my new portfolio website.</p>
<h2 id="heading-create-nuxt-3-projectcreate-nuxt-3-project">Create Nuxt 3 project<a class="post-section-overview" href="#create-nuxt-3-project"></a></h2>
<p>To create a new Nuxt 3 project, we need to run this command in our terminal:</p>
<pre><code>npx nuxi <span class="hljs-keyword">init</span> nuxt3-app
</code></pre><h2 id="heading-add-tailwind-css-3add-tailwind-css-3">Add Tailwind CSS 3<a class="post-section-overview" href="#add-tailwind-css-3"></a></h2>
<p>Next, we add the <a target="_blank" href="https://tailwindcss.nuxtjs.org">nuxt/tailwind</a> module, which provides a <a target="_blank" href="https://tailwindcss.nuxtjs.org/releases/#Nuxt%203%20and%20Tailwindcss%203%20support">prerelease version</a> that supports Nuxt 3 and Tailwind CSS v3:</p>
<pre><code><span class="hljs-attribute">npm</span> install --save-dev @nuxtjs/tailwindcss@<span class="hljs-number">5</span>.<span class="hljs-number">0</span>.<span class="hljs-number">0</span>-<span class="hljs-number">4</span>
</code></pre><p>Then we need to add this module to the <code>buildModules</code> section in <code>nuxt.config.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { defineNuxtConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'nuxt3'</span>;

<span class="hljs-comment">// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineNuxtConfig({
  <span class="hljs-attr">buildModules</span>: [<span class="hljs-string">'@nuxtjs/tailwindcss'</span>],});
</code></pre>
<p>Now, we can create the Tailwind configuration file <code>tailwind.config.js</code> by running the following command:</p>
<pre><code>npx tailwindcss <span class="hljs-keyword">init</span>
</code></pre><p>Let’s add a basic CSS file at <code>./assets/css/tailwind.css</code> (see <a target="_blank" href="https://tailwindcss.nuxtjs.org/setup#tailwind-files">official docs</a> for further configuration options):</p>
<pre><code class="lang-css"><span class="hljs-keyword">@tailwind</span> base;
<span class="hljs-keyword">@tailwind</span> components;
<span class="hljs-keyword">@tailwind</span> utilities;

<span class="hljs-selector-class">.theme-light</span> {
  <span class="hljs-attribute">--background</span>: <span class="hljs-number">#f8f8f8</span>;
  <span class="hljs-attribute">--text</span>: <span class="hljs-number">#313131</span>;
}

<span class="hljs-selector-class">.theme-dark</span> {
  <span class="hljs-attribute">--background</span>: <span class="hljs-number">#313131</span>;
  <span class="hljs-attribute">--text</span>: <span class="hljs-number">#f8f8f8</span>;
}
</code></pre>
<p>We define two CSS classes for the dark and light theme. <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties">CSS variables</a> (indicated by <code>--</code>) are used to change CSS values based on the selected theme dynamically.</p>
<p>Therefore, we need to define these colors in our <code>tailwind.conf.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">content</span>: [
    <span class="hljs-string">`components/**/*.{vue,js,ts}`</span>,
    <span class="hljs-string">`layouts/**/*.vue`</span>,
    <span class="hljs-string">`pages/**/*.vue`</span>,
    <span class="hljs-string">`app.vue`</span>,
    <span class="hljs-string">`plugins/**/*.{js,ts}`</span>,
    <span class="hljs-string">`nuxt.config.{js,ts}`</span>,
  ],
  <span class="hljs-attr">theme</span>: {
    <span class="hljs-attr">extend</span>: {
      <span class="hljs-attr">colors</span>: { <span class="hljs-attr">themeBackground</span>: <span class="hljs-string">'var(--background)'</span>, <span class="hljs-attr">themeText</span>: <span class="hljs-string">'var(--text)'</span>, }, },
  },
  <span class="hljs-attr">plugins</span>: [],
};
</code></pre>
<h2 id="heading-implement-theme-switchimplement-theme-switch">Implement Theme Switch<a class="post-section-overview" href="#implement-theme-switch"></a></h2>
<p>Let’s start to implement a theme switch by adding this simple template to our <code>app.vue</code> component:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
    <span class="hljs-attr">:class</span>=<span class="hljs-string">"{
      'theme-light': !darkMode,
      'theme-dark': darkMode,
    }"</span>
    <span class="hljs-attr">class</span>=<span class="hljs-string">"h-screen bg-themeBackground p-5"</span>
  &gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-themeText"</span>&gt;</span>Nuxt 3 Tailwind Dark Mode Demo<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Toggle</span> <span class="hljs-attr">v-model</span>=<span class="hljs-string">"darkMode"</span> <span class="hljs-attr">off-label</span>=<span class="hljs-string">"Light"</span> <span class="hljs-attr">on-label</span>=<span class="hljs-string">"Dark"</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>On the <code>div</code> container element, we dynamically set <code>theme-light</code> or <code>theme-dark</code> CSS class based on the reactive <code>darkMode</code> variable value, which we will implement later in the <code>script</code> part of the component.</p>
<p>The <code>h1</code> and container <code>div</code> elements use our Tailwind CSS classes <code>bg-themeBackground</code> and <code>text-themeText</code> to use theme-specific colors for the background and text color.</p>
<p>Additionally, we use the <a target="_blank" href="https://github.com/vueform/toggle">Vue 3 Toggle</a> library to switch between our themes.</p>
<p>Let’s take a look at the <code>script</code> part of <code>app.vue</code>:</p>
<pre><code class="lang-ts">&lt;script setup lang=<span class="hljs-string">"ts"</span>&gt;
<span class="hljs-keyword">import</span> Toggle <span class="hljs-keyword">from</span> <span class="hljs-string">'@vueform/toggle'</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'#app'</span>;
<span class="hljs-keyword">import</span> { onMounted, watch } <span class="hljs-keyword">from</span> <span class="hljs-string">'@vue/runtime-core'</span>;

<span class="hljs-keyword">type</span> Theme = <span class="hljs-string">'light'</span> | <span class="hljs-string">'dark'</span>;

<span class="hljs-keyword">const</span> LOCAL_STORAGE_THEME_KEY = <span class="hljs-string">'theme'</span>;

<span class="hljs-keyword">const</span> darkMode = useState(<span class="hljs-string">'theme'</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-literal">false</span>);

<span class="hljs-keyword">const</span> setTheme = <span class="hljs-function">(<span class="hljs-params">newTheme: Theme</span>) =&gt;</span> {
  <span class="hljs-built_in">localStorage</span>.setItem(LOCAL_STORAGE_THEME_KEY, newTheme);
  darkMode.value = newTheme === <span class="hljs-string">'dark'</span>;
};

onMounted(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> isDarkModePreferred = <span class="hljs-built_in">window</span>.matchMedia(
    <span class="hljs-string">'(prefers-color-scheme: dark)'</span>
  ).matches;

  <span class="hljs-keyword">const</span> themeFromLocalStorage = <span class="hljs-built_in">localStorage</span>.getItem(
    LOCAL_STORAGE_THEME_KEY
  ) <span class="hljs-keyword">as</span> Theme;

  <span class="hljs-keyword">if</span> (themeFromLocalStorage) {
    setTheme(themeFromLocalStorage);
  } <span class="hljs-keyword">else</span> {
    setTheme(isDarkModePreferred ? <span class="hljs-string">'dark'</span> : <span class="hljs-string">'light'</span>);
  }
});

watch(darkMode, <span class="hljs-function"><span class="hljs-params">selected</span> =&gt;</span> {
  setTheme(selected ? <span class="hljs-string">'dark'</span> : <span class="hljs-string">'light'</span>);
});
&lt;/script&gt;
</code></pre>
<p>We store the selected theme value in <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector/Local_Storage_Session_Storage">Local Storage</a> and use <a target="_blank" href="https://v3.nuxtjs.org/docs/usage/state">useState</a> to define a reactive variable called <code>darkMode</code>:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> darkMode = useState(<span class="hljs-string">'theme'</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-literal">false</span>);
</code></pre>
<p>If the component is mounted, we first detect if the user has requested light or dark color theme by using <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">the CSS media feature “prefers-color-scheme”</a>:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> isDarkModePreferred = <span class="hljs-built_in">window</span>.matchMedia(
  <span class="hljs-string">'(prefers-color-scheme: dark)'</span>
).matches;
</code></pre>
<p>Then we set the theme value based on the local storage value:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> setTheme = <span class="hljs-function">(<span class="hljs-params">newTheme: Theme</span>) =&gt;</span> {
  <span class="hljs-built_in">localStorage</span>.setItem(LOCAL_STORAGE_THEME_KEY, newTheme);
  darkMode.value = newTheme === <span class="hljs-string">'dark'</span>;
};

onMounted(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> isDarkModePreferred = <span class="hljs-built_in">window</span>.matchMedia(
    <span class="hljs-string">'(prefers-color-scheme: dark)'</span>
  ).matches;

  <span class="hljs-keyword">const</span> themeFromLocalStorage = <span class="hljs-built_in">localStorage</span>.getItem( LOCAL_STORAGE_THEME_KEY ) <span class="hljs-keyword">as</span> Theme; <span class="hljs-keyword">if</span> (themeFromLocalStorage) { setTheme(themeFromLocalStorage); } <span class="hljs-keyword">else</span> { setTheme(isDarkModePreferred ? <span class="hljs-string">'dark'</span> : <span class="hljs-string">'light'</span>); }});
</code></pre>
<p>This the complete <code>app.vue</code> component code:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
    <span class="hljs-attr">:class</span>=<span class="hljs-string">"{
      'theme-light': !darkMode,
      'theme-dark': darkMode,
    }"</span>
    <span class="hljs-attr">class</span>=<span class="hljs-string">"h-screen bg-themeBackground p-5"</span>
  &gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-themeText"</span>&gt;</span>Nuxt 3 Tailwind Dark Mode Demo<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Toggle</span> <span class="hljs-attr">v-model</span>=<span class="hljs-string">"darkMode"</span> <span class="hljs-attr">off-label</span>=<span class="hljs-string">"Light"</span> <span class="hljs-attr">on-label</span>=<span class="hljs-string">"Dark"</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> Toggle <span class="hljs-keyword">from</span> <span class="hljs-string">'@vueform/toggle'</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'#app'</span>;
<span class="hljs-keyword">import</span> { onMounted, watch } <span class="hljs-keyword">from</span> <span class="hljs-string">'@vue/runtime-core'</span>;

type Theme = <span class="hljs-string">'light'</span> | <span class="hljs-string">'dark'</span>;

<span class="hljs-keyword">const</span> LOCAL_STORAGE_THEME_KEY = <span class="hljs-string">'theme'</span>;

<span class="hljs-keyword">const</span> darkMode = useState(<span class="hljs-string">'theme'</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-literal">false</span>);

<span class="hljs-keyword">const</span> setTheme = <span class="hljs-function">(<span class="hljs-params">newTheme: Theme</span>) =&gt;</span> {
  <span class="hljs-built_in">localStorage</span>.setItem(LOCAL_STORAGE_THEME_KEY, newTheme);
  darkMode.value = newTheme === <span class="hljs-string">'dark'</span>;
};

onMounted(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> isDarkModePreferred = <span class="hljs-built_in">window</span>.matchMedia(
    <span class="hljs-string">'(prefers-color-scheme: dark)'</span>
  ).matches;

  <span class="hljs-keyword">const</span> themeFromLocalStorage = <span class="hljs-built_in">localStorage</span>.getItem(
    LOCAL_STORAGE_THEME_KEY
  ) <span class="hljs-keyword">as</span> Theme;

  <span class="hljs-keyword">if</span> (themeFromLocalStorage) {
    setTheme(themeFromLocalStorage);
  } <span class="hljs-keyword">else</span> {
    setTheme(isDarkModePreferred ? <span class="hljs-string">'dark'</span> : <span class="hljs-string">'light'</span>);
  }
});

watch(darkMode, <span class="hljs-function"><span class="hljs-params">selected</span> =&gt;</span> {
  setTheme(selected ? <span class="hljs-string">'dark'</span> : <span class="hljs-string">'light'</span>);
});
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"@vueform/toggle/themes/default.css"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
</code></pre>
<p>Now we can use run the following command to start our Nuxt app in development mode:</p>
<pre><code><span class="hljs-built_in">npm</span> run dev
</code></pre><p>Finally, we can test our dark mode theme switch at <code>http://localhost:3000</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645434329568/psyoJiFWB.gif" alt="Theme Switch In Action" /></p>
<h2 id="heading-stackblitz-demostackblitz-demo">StackBlitz Demo<a class="post-section-overview" href="#stackblitz-demo"></a></h2>
<p>My simple demo is available as interactive StackBlitz demo:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://stackblitz.com/github/bnmipwkmo?embed=1">https://stackblitz.com/github/bnmipwkmo?embed=1</a></div>
<p>Alternatively, you could also use the <a target="_blank" href="https://color-mode.nuxtjs.org/">color-mode</a> module that supports Nuxt Bridge and Nuxt 3.</p>
<h2 id="heading-conclusionconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>This article showed you how to create a simple dark mode switch in Nuxt 3 with Tailwind CSS v3. You can expect more Nuxt 3 posts in the following months as I plan to blog about interesting topics that I discover while I rewrite my portfolio website.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my weekly Vue.js newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Vue 3 Desktop App With Pinia, Electron and Quasar]]></title><description><![CDATA[Recently, I planned to rewrite my “Scrum Daily Standup Picker” Electron application in Vue 3. I wrote the initial release in Angular, but I wanted to refactor the code base and rewrite it in Vue 3.
Why? Because I love Vue and want to have a public sh...]]></description><link>https://blog.mokkapps.de/building-a-vue-3-desktop-app-with-pinia-electron-and-quasar</link><guid isPermaLink="true">https://blog.mokkapps.de/building-a-vue-3-desktop-app-with-pinia-electron-and-quasar</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[Electron]]></category><category><![CDATA[Quasar Framework]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[vue]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Mon, 14 Feb 2022 18:48:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1644864524528/bWIw-NL6t.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I planned to rewrite my <a target="_blank" href="https://github.com/Mokkapps/scrum-daily-standup-picker/">“Scrum Daily Standup Picker” Electron application</a> in Vue 3. I wrote the initial release in Angular, but I wanted to refactor the code base and rewrite it in Vue 3.</p>
<p>Why? Because I love Vue and want to have a public showcase that I can reference to potential customers.</p>
<h2 id="heading-why-quasarwhy-quasar">Why Quasar?<a class="post-section-overview" href="#why-quasar"></a></h2>
<p><a target="_blank" href="https://quasar.dev/">Quasar</a> is an MIT licensed open-source Vue.js based framework that targets SPA, SSR, PWA, mobile app, desktop app, and browser extension all using one codebase. It handles the build setup and provides a complete collection of Material Design compliant UI components.</p>
<p>Quasar’s motto is:</p>
<blockquote>
<p>Write code once and simultaneously deploy it as a website, a Mobile App and/or an Electron App.</p>
</blockquote>
<p>Using Quasar drastically saves development time due to these reasons:</p>
<ul>
<li>It’s based on Vue.js.</li>
<li>It provides many UI components that follow Material Design guidelines.</li>
<li>It has a regular release cycle inclusive of new features.</li>
<li>It provides support for each build mode (SPA, SSR, PWA, Mobile app, Desktop app &amp; Browser Extension).</li>
<li>It has its own CLI that provides a pleasant developer experience. For example, we can build our application as SPA, mobile, or desktop app within the same project folder.</li>
</ul>
<p><a target="_blank" href="https://quasar.dev/introduction-to-quasar">Read more</a> about why Quasar might be a good choice for your next project.</p>
<h2 id="heading-install-quasar-cliinstall-quasar-cli">Install Quasar CLI<a class="post-section-overview" href="#install-quasar-cli"></a></h2>
<p>The source code for the following demo is <a target="_blank" href="https://github.com/Mokkapps/quasar-electron-vue3-pinia-demo">available at GitHub</a></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Node.js &gt;=12.22.1 is required.</span>

$ yarn global add @quasar/cli
<span class="hljs-comment"># or</span>
$ npm install -g @quasar/cli
</code></pre>
<p>Let’s start by creating a new project using the Quasar CLI:</p>
<pre><code class="lang-bash">▶ quasar create vue3-electron-demo

  ___
 / _ \ _ _ ______  ___ ___
| | | | | | |/ _` / __|/ _` | <span class="hljs-string">'__ |
| |_| | |_| | (_| \__ \ (_| | |
 \ __\_\\__ ,_|\ __,_|___ /\__,_|_|

? Project name (internal usage for dev) vue3-electron-demo
? Project product name (must start with letter if building mobile apps) Quasar App
? Project description A Quasar Framework app
? Author Michael Hoffmann &lt;michael.hoffmann@mokkapps.de&gt;
? Pick your CSS preprocessor: SCSS
? Check the features needed for your project: ESLint (recommended), TypeScript
? Pick a component style: Composition
? Pick an ESLint preset: Prettier
? Continue to install project dependencies after the project has been created? (recommended) NPM</span>
</code></pre>
<p>We chose <a target="_blank" href="https://sass-lang.com/">SCSS</a> as our CSS preprocessor, <a target="_blank" href="https://eslint.org/">ESLint</a> &amp; <a target="_blank" href="https://www.typescriptlang.org/">Typescript</a> as additional features, <a target="_blank" href="https://vuejs.org/guide/introduction.html#api-styles">Vue 3’s Composition API</a> and <a target="_blank" href="https://prettier.io/">Prettier</a> for code formatting.</p>
<p>Do not choose Vuex as we will add another state library in the next chapter. If you accidentally added Vuex, remove it manually from your <code>package.json</code>.</p>
<p><a target="_blank" href="https://quasar.dev/quasar-cli/installation">Read the official docs</a> for additional information about the Quasar CLI.</p>
<h2 id="heading-add-pinia-as-vue-store-libraryadd-pinia-as-vue-store-library">Add Pinia as Vue store library<a class="post-section-overview" href="#add-pinia-as-vue-store-library"></a></h2>
<p>We’ll use <a target="_blank" href="https://pinia.vuejs.org/">Pinia</a> as Vue store library, which is now the recommended state library for Vue.</p>
<p>First, we need to install Pinia:</p>
<pre><code class="lang-bash">yarn add pinia
<span class="hljs-comment"># or with npm</span>
npm install pinia
</code></pre>
<p>To be able to register Pinia at our Vue application instance we need to create a <a target="_blank" href="https://quasar.dev/quasar-cli/boot-files">Quasar Boot File</a>:</p>
<blockquote>
<p>A common use case for Quasar applications is to run code before the root Vue app instance is instantiated, like injecting and initializing your own dependencies (examples: Vue components, libraries…) or simply configuring some startup code of your app.</p>
</blockquote>
<p>Our boot file is called <code>pinia.ts</code> and is located at <code>src/boot</code>:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { boot } <span class="hljs-keyword">from</span> <span class="hljs-string">'quasar/wrappers'</span>;
<span class="hljs-keyword">import</span> { createPinia } <span class="hljs-keyword">from</span> <span class="hljs-string">'pinia'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> boot(<span class="hljs-function">(<span class="hljs-params">{ app }</span>) =&gt;</span> {
  app.use(createPinia());
});
</code></pre>
<p>We also need to add this new file to <code>quasar.conf.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-built_in">module</span>.exports = configure(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">return</span> {
    ...
    <span class="hljs-comment">// app boot file (/src/boot)</span>
    <span class="hljs-comment">// --&gt; boot files are part of "main.js"</span>
    <span class="hljs-comment">// https://quasar.dev/quasar-cli/boot-files</span>
    <span class="hljs-attr">boot</span>: [<span class="hljs-string">'pinia'</span>], ...
  }
}
</code></pre>
<p>Now, we can create a new folder called <code>pinia</code> in <code>src</code>.</p>
<p>We cannot name this folder <code>store</code> as this name is reserved for the official Vuex integration.</p>
<p>A basic store could look like this:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { defineStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'pinia'</span>;

<span class="hljs-comment">// useStore could be anything like useUser, useCart</span>
<span class="hljs-comment">// the first argument is a unique id of the store across your application</span>
<span class="hljs-keyword">const</span> useStore = defineStore(<span class="hljs-string">'storeId'</span>, {
  <span class="hljs-attr">state</span>: <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">counter</span>: <span class="hljs-number">0</span>,
      <span class="hljs-attr">lastName</span>: <span class="hljs-string">'Michael'</span>,
      <span class="hljs-attr">firstName</span>: <span class="hljs-string">'Michael'</span>,
    };
  },
  <span class="hljs-attr">getters</span>: {
    <span class="hljs-attr">fullName</span>: <span class="hljs-function"><span class="hljs-params">state</span> =&gt;</span> <span class="hljs-string">`<span class="hljs-subst">${state.firstName}</span> <span class="hljs-subst">${state.lastName}</span>`</span>,
  },
  <span class="hljs-attr">actions</span>: {
    increment() {
      <span class="hljs-built_in">this</span>.counter++;
    },
  },
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> useStore;
</code></pre>
<p>We can use this store in any Vue component:</p>
<pre><code class="lang-vue">&lt;template&gt;Counter: {{ store.counter }}&lt;/template&gt;

&lt;script setup lang="ts"&gt;
import { useStore } from '@/stores/counter';

const store = useStore();
&lt;/script&gt;
</code></pre>
<p>Now we can run the Vue application using the Quasar CLI:</p>
<pre><code class="lang-bash">quasar dev
</code></pre>
<p>The Vue application is served at <code>http://localhost:8080</code>:</p>
<p><a target="_blank" href="/static/db6abff278f4c712bec727fe70763d42/4d4fd/quasar-vue-dev.jpg"><img src="https://www.mokkapps.de/static/db6abff278f4c712bec727fe70763d42/15ec7/quasar-vue-dev.jpg" alt="Quasar Dev Mode" /></a>
<em>Quasar Dev Mode</em></p>
<h2 id="heading-setup-electronsetup-electron">Setup Electron<a class="post-section-overview" href="#setup-electron"></a></h2>
<p>Read this <a target="_blank" href="https://quasar.dev/quasar-cli/developing-electron-apps/introduction">introduction</a> if you are new to Electron.</p>
<p>To develop/build a Quasar Electron app, we need to add the Electron mode to our Quasar project:</p>
<pre><code class="lang-bash">$ quasar mode add electron
</code></pre>
<p>Every Electron app has two threads: the main thread (deals with the window and initialization code – from the newly created folder <code>/src-electron</code>) and the renderer thread (which deals with the actual content of your app from <code>/src</code>).</p>
<p>The new folder has the following structure:</p>
<pre><code>.
└── src-electron/
├── icons/ <span class="hljs-comment"># Icons of your app for all platforms</span>
| ├── icon.icns <span class="hljs-comment"># Icon file for Darwin (MacOS) platform</span>
| ├── icon.ico <span class="hljs-comment"># Icon file for win32 (Windows) platform</span>
| └── icon.png <span class="hljs-comment"># Tray icon file for all platforms</span>
├── electron-preload.js <span class="hljs-comment"># (or .ts) Electron preload script (injects Node.js stuff into renderer thread)</span>
└── electron-main.js <span class="hljs-comment"># (or .ts) Main thread code</span>
</code></pre><p>Now we are ready to start our Electron application:</p>
<pre><code class="lang-bash">$ quasar dev -m electron
</code></pre>
<p>This command will open up an Electron window which will render your app along with Developer Tools opened side by side:</p>
<p><a target="_blank" href="/static/b73a761d0abcde3dc1c285659416ec37/40267/quasar-electron-dev.jpg"><img src="https://www.mokkapps.de/static/b73a761d0abcde3dc1c285659416ec37/15ec7/quasar-electron-dev.jpg" alt="Quasar Electron Dev" /></a>
<em>Quasar Electron Dev</em></p>
<p><a target="_blank" href="https://quasar.dev/quasar-cli/developing-electron-apps/">Read the official docs</a> for additional and detailed information about developing Electron apps with Quasar.</p>
<h2 id="heading-control-electron-from-vue-codecontrol-electron-from-vue-code">Control Electron from Vue code<a class="post-section-overview" href="#control-electron-from-vue-code"></a></h2>
<p>If we want to use Electron features like opening a file dialog, we need to write some code to be able to access Electron’s API.</p>
<p>For example, if we want to show a dialog to open files, Electron provides the <a target="_blank" href="https://www.electronjs.org/docs/latest/api/dialog/">dialog API</a> to display native system dialogs for opening and saving files, alerting, etc.</p>
<p>First, we need to install <code>@electron/remote</code>:</p>
<pre><code class="lang-bash">npm install -D @electron/remote
</code></pre>
<p>Then we need to modify <code>src-electron/electron-main.js</code> and initialize <code>@electron/remote</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { app, BrowserWindow, nativeTheme } <span class="hljs-keyword">from</span> <span class="hljs-string">'electron'</span>
<span class="hljs-keyword">import</span> { initialize, enable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@electron/remote/main'</span><span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">'path'</span>
<span class="hljs-keyword">import</span> os <span class="hljs-keyword">from</span> <span class="hljs-string">'os'</span>

initialize();
<span class="hljs-keyword">let</span> mainWindow;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createWindow</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">/**
   * Initial window options
   */</span>
  mainWindow = <span class="hljs-keyword">new</span> BrowserWindow({
    <span class="hljs-attr">icon</span>: path.resolve(__dirname, <span class="hljs-string">'icons/icon.png'</span>), <span class="hljs-comment">// tray icon</span>
    <span class="hljs-attr">width</span>: <span class="hljs-number">1000</span>,
    <span class="hljs-attr">height</span>: <span class="hljs-number">600</span>,
    <span class="hljs-attr">useContentSize</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">webPreferences</span>: {
      <span class="hljs-attr">contextIsolation</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-comment">// More info: /quasar-cli/developing-electron-apps/electron-preload-script</span>
      <span class="hljs-attr">preload</span>: path.resolve(__dirname, process.env.QUASAR_ELECTRON_PRELOAD)
    }
  })

  <span class="hljs-comment">// ....</span>

  enable(mainWindow.webContents);}
</code></pre>
<p>If we want to use Electron API from our Vue code we need to add some code to <code>src-electron/electron-preload.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { contextBridge } <span class="hljs-keyword">from</span> <span class="hljs-string">'electron'</span>;
<span class="hljs-keyword">import</span> { dialog } <span class="hljs-keyword">from</span> <span class="hljs-string">'@electron/remote'</span>;
<span class="hljs-comment">// 'electronApi' will be available on the global window context</span>
contextBridge.exposeInMainWorld(<span class="hljs-string">'electronApi'</span>, {
  <span class="hljs-attr">openFileDialog</span>: <span class="hljs-keyword">async</span> (title, folder, filters) =&gt; {
    <span class="hljs-comment">// calling showOpenDialog from Electron API: https://www.electronjs.org/docs/latest/api/dialog/</span>
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> dialog.showOpenDialog({ title, filters, <span class="hljs-attr">properties</span>: [<span class="hljs-string">'openFile'</span>, <span class="hljs-string">'multiSelections'</span>], }); <span class="hljs-keyword">return</span> response.filePaths;
  }
});
</code></pre>
<p>Next we create <code>src/api/electron-api.ts</code> to access this code from within our Vue application:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> ElectronFileFilter {
  name: <span class="hljs-built_in">string</span>;
  extensions: <span class="hljs-built_in">string</span>[];
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> ElectronApi {
  openFileDialog: <span class="hljs-function">(<span class="hljs-params">
    title: <span class="hljs-built_in">string</span>,
    folder: <span class="hljs-built_in">string</span>,
    filters: ElectronFileFilter
  </span>) =&gt;</span> <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">string</span>[]&gt;;
}

<span class="hljs-comment">// eslint-disable-next-line @typescript-eslint/ban-ts-comment</span>
<span class="hljs-comment">// @ts-ignore</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> electronApi: ElectronApi = (<span class="hljs-built_in">window</span> <span class="hljs-keyword">as</span> { electronApi: ElectronApi })
  .electronApi;
</code></pre>
<p>Now we can use this API anywhere in our Vue component:</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;q-btn @click="openElectronFileDialog"&gt;Open Electron File Dialog&lt;/q-btn&gt;
&lt;/template&gt;

&lt;script lang="ts"&gt;
import { defineComponent } from 'vue';
import { electronApi } from 'src/api/electron-api';

export default defineComponent({
  name: 'PageIndex',
  components: { },
  setup() {
    const openElectronFileDialog = async () =&gt; {
      return electronApi.openFileDialog('Test', 'folder', { name: 'images', extensions: ['jpg'] });
    };

    return { openElectronFileDialog };
  },
});
&lt;/script&gt;
</code></pre>
<p>Clicking on the button should now open the native OS file dialog:</p>
<p><a target="_blank" href="/static/0659d14deafccbb62c5c42fbb3754aa7/85c49/quasar-electron-file-dialog.jpg"><img src="https://www.mokkapps.de/static/0659d14deafccbb62c5c42fbb3754aa7/15ec7/quasar-electron-file-dialog.jpg" alt="Electron File Dialog" /></a>
<em>Electron File Dialog</em></p>
<h2 id="heading-conclusionconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>Quasar allows us to quickly develop Electron desktop applications in Vue with high-quality UI components that follow Material Design guidelines.</p>
<p>The most significant advantage against a custom Electron + Vue boilerplate project from GitHub is that Quasar has a regular release cycle and provides <a target="_blank" href="https://quasar.dev/quasar-cli/developing-electron-apps/electron-upgrade-guide">upgrade guides</a> for older versions.</p>
<p>Take a look at my <a target="_blank" href="https://github.com/Mokkapps/scrum-daily-standup-picker">“Scrum Daily Standup Picker” GitHub repository</a> to see a more complex “Quasar-Electron-Vue3-Typescript-Pinia” project. The demo source code for the following demo is <a target="_blank" href="https://github.com/Mokkapps/quasar-electron-vue3-pinia-demo">available at GitHub</a>.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[The 10 Favorite Features of My Developer Portfolio Website]]></title><description><![CDATA[Inspired by Braydon Coyer’s new blogfolio, I’ve added some excellent new features to my portfolio website.
In this article, I want to demonstrate the ten favorite features of my blogfolio.
Table of Contents

1. Stats page
2. Article Reactions
3. Auto...]]></description><link>https://blog.mokkapps.de/the-10-favorite-features-of-my-developer-portfolio-website</link><guid isPermaLink="true">https://blog.mokkapps.de/the-10-favorite-features-of-my-developer-portfolio-website</guid><category><![CDATA[portfolio]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Blogging]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Tue, 04 Jan 2022 10:44:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1641294067043/fNMGuJPiE.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Inspired by <a target="_blank" href="https://braydoncoyer.dev/blog/introducing-my-new-blogfolio">Braydon Coyer’s new blogfolio</a>, I’ve added some excellent new features to my portfolio website.</p>
<p>In this article, I want to demonstrate the ten favorite features of my blogfolio.</p>
<h2 id="heading-table-of-contentstable-of-contents">Table of Contents<a class="post-section-overview" href="#table-of-contents"></a></h2>
<ul>
<li><a class="post-section-overview" href="#1-stats-page">1. Stats page</a></li>
<li><a class="post-section-overview" href="#2-article-reactions">2. Article Reactions</a></li>
<li><a class="post-section-overview" href="#3-automated-open-graph-images">3. Automated Open Graph Images</a></li>
<li><a class="post-section-overview" href="#4-mark-article-as-read">4. Mark Article as Read</a></li>
<li><a class="post-section-overview" href="#5-intelligent-article-suggestions">5. Intelligent Article Suggestions</a></li>
<li><a class="post-section-overview" href="#6-article-search-options">6. Article Search Options</a></li>
<li><a class="post-section-overview" href="#7-prism-code-highlighting">7. Prism Code Highlighting</a></li>
<li><a class="post-section-overview" href="#8-mdx">8. MDX</a></li>
<li><a class="post-section-overview" href="#9-generate-scripts">9. Generate Scripts</a></li>
<li><a class="post-section-overview" href="#10-open-source-analytics">10. Open Source Analytics</a></li>
<li><a class="post-section-overview" href="#conclusion">Conclusion</a></li>
</ul>
<h2 id="heading-1-stats-page1-stats-page">1. Stats page<a class="post-section-overview" href="#1-stats-page"></a></h2>
<p>Inspired by <a target="_blank" href="https://sld.codes/">SLD</a> and <a target="_blank" href="http://braydoncoyer.dev/stats">Braydon Coyer</a>, I’ve added a <a target="_blank" href="https://mokkapps.de/stats">Stats page on my site</a>.</p>
<p>It shows statistics about the site itself, for example, how many active visitors are currently on the site and how many have visited it in total.</p>
<p>Additionally, it shows some information about my social media channels like the follower count of <a target="_blank" href="https://github.com/mokkapps">GitHub</a>, <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a>, <a target="_blank" href="https://dev.to/mokkapps">Dev.to</a>, and more.</p>
<p>I use <a target="_blank" href="https://mokkapps.de/categories/aws">AWS Amplify Serverless Functions</a> to access a variety of APIs to provide the necessary data for this site.</p>
<p><a target="_blank" href="/static/b24619af5a80a7707218b496edd44f55/d9c39/stats.jpg"><img src="https://www.mokkapps.de/static/b24619af5a80a7707218b496edd44f55/15ec7/stats.jpg" alt="Stats Page" /></a></p>
<h2 id="heading-2-article-reactions2-article-reactions">2. Article Reactions<a class="post-section-overview" href="#2-article-reactions"></a></h2>
<p>Built with <a target="_blank" href="https://supabase.com/">Supabase</a> and AWS Amplify Serverless Functions, readers of my articles can now react to the article with the clap emoji.</p>
<p>Additionally, I use the same database table to store the number of page views.</p>
<p><a target="_blank" href="/static/037a95b88ecc0956f83d183a9ed54beb/d9c39/article-reactions.jpg"><img src="https://www.mokkapps.de/static/037a95b88ecc0956f83d183a9ed54beb/15ec7/article-reactions.jpg" alt="Article Reactions" /></a></p>
<h2 id="heading-3-automated-open-graph-images3-automated-open-graph-images">3. Automated Open Graph Images<a class="post-section-overview" href="#3-automated-open-graph-images"></a></h2>
<p>I use <a target="_blank" href="https://braydoncoyer.dev/blog/how-to-dynamically-create-open-graph-images-with-cloudinary-and-next.js">Braydon’s approach</a> to automatically generate <a target="_blank" href="https://ogp.me/">Open Graph</a> images for certain pages using the <a target="_blank" href="https://cloudinary.com/documentation/cloudinary_references">Cloudinary API</a>.</p>
<p>The code grabs the site’s title and generates an Open Graph image using Cloudinary API.</p>
<p>The following image shows such an automatically generated image that I use on my website:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1641294060514/myAlMZTac.jpeg" alt="Open Graph Image With Cloudinary API" /></p>
<h2 id="heading-4-mark-article-as-read4-mark-article-as-read">4. Mark Article as Read<a class="post-section-overview" href="#4-mark-article-as-read"></a></h2>
<p>Visitors of my website can see at a glimpse which articles they’ve already read. It’s a nice little feature for recurring readers of my blog.</p>
<p><a target="_blank" href="/static/0e2facdcc4c5e2976a81a3e133634101/d9c39/article-read.jpg"><img src="https://www.mokkapps.de/static/0e2facdcc4c5e2976a81a3e133634101/15ec7/article-read.jpg" alt="Read blog articles" /></a></p>
<h2 id="heading-5-intelligent-article-suggestions5-intelligent-article-suggestions">5. Intelligent Article Suggestions<a class="post-section-overview" href="#5-intelligent-article-suggestions"></a></h2>
<p>If a reader of a blog article reaches the end of the article, he will see four similar articles. They are selected by checking how many categories match between the articles.</p>
<p><a target="_blank" href="/static/b48a2b61d17c3909457a50f852af55a7/d9c39/similar-articles.jpg"><img src="https://www.mokkapps.de/static/b48a2b61d17c3909457a50f852af55a7/15ec7/similar-articles.jpg" alt="Suggested Articles" /></a></p>
<h2 id="heading-6-article-search-options6-article-search-options">6. Article Search Options<a class="post-section-overview" href="#6-article-search-options"></a></h2>
<p>I provide multiple ways to search for blog articles:</p>
<ol>
<li>All articles are available on the <a target="_blank" href="https://mokkapps.de/blog">blog page</a>, and you can scroll or use the browser search to find an article.</li>
<li>Use the <a target="_blank" href="https://mokkapps.de/minimal-blog-list">minimal list</a>, which shows all blog posts grouped in years by title.</li>
<li>Use <a target="_blank" href="https://www.google.com/search?q=site%3Amokkapps.de%2Fblog">Google</a>.</li>
</ol>
<p><a target="_blank" href="/static/0906bfe39510ea30b471b45de778558a/d9c39/blog-search.jpg"><img src="https://www.mokkapps.de/static/0906bfe39510ea30b471b45de778558a/15ec7/blog-search.jpg" alt="Article Search" /></a></p>
<h2 id="heading-7-prism-code-highlighting7-prism-code-highlighting">7. Prism Code Highlighting<a class="post-section-overview" href="#7-prism-code-highlighting"></a></h2>
<p>I invested some time to create beautiful code snippets on my blog posts, as they are an essential part of my articles.</p>
<p>I use <a target="_blank" href="https://prismjs.com/">Prism</a> with the <a target="_blank" href="https://www.gatsbyjs.com/plugins/gatsby-remark-prismjs/">Gatsby Prism Remark plugin</a> to show code blocks in my markdown files:</p>
<pre><code class="lang-js"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getCategoryDisplayText = <span class="hljs-function"><span class="hljs-params">category</span> =&gt;</span> {
  <span class="hljs-keyword">if</span> (category === <span class="hljs-string">'aws'</span>) { <span class="hljs-keyword">return</span> category.toUpperCase(); }
  <span class="hljs-keyword">if</span> (category.includes(<span class="hljs-string">'-js'</span>)) {
    <span class="hljs-keyword">const</span> name = category.split(<span class="hljs-string">'-'</span>)[<span class="hljs-number">0</span>];
    <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${capitalize(name)}</span>.js`</span>;
  }

  <span class="hljs-keyword">return</span> capitalize(category);
};
</code></pre>
<p>I can highlight certain lines of code, and I show the programming language as a nice badge on the top right.</p>
<h2 id="heading-8-mdx8-mdx">8. MDX<a class="post-section-overview" href="#8-mdx"></a></h2>
<p><a target="_blank" href="https://mdxjs.com/">MDX</a> is very powerful, and I use it for my tips page, where I inject the following React component into my Markdown files to create a beautiful comparison of two code blocks:</p>
<p><img src="https://www.mokkapps.de/blog/the-10-favorite-features-of-my-developer-portfolio-website/" alt="Only available in the original post" /></p>
<h2 id="heading-9-generate-scripts9-generate-scripts">9. Generate Scripts<a class="post-section-overview" href="#9-generate-scripts"></a></h2>
<p>Inspired by <a target="_blank" href="https://github.com/kentcdodds">Kent C. Dodds</a>, I use <a target="_blank" href="https://github.com/Mokkapps/website/tree/master/scripts/generate">multiple JS scripts</a> to generate boilerplate files for new blog posts and tips.</p>
<p>For example, the <code>blogpost.js</code> script will generate a similar output in the console:</p>
<pre><code><span class="hljs-string">?</span> <span class="hljs-string">Title</span> <span class="hljs-string">this</span> <span class="hljs-string">is</span> <span class="hljs-string">a</span> <span class="hljs-string">test</span> <span class="hljs-string">to</span> <span class="hljs-string">see</span> <span class="hljs-string">if</span> <span class="hljs-string">my</span> <span class="hljs-string">script</span> <span class="hljs-string">is</span> <span class="hljs-string">awesome</span>
<span class="hljs-string">?</span> <span class="hljs-string">Categories</span> <span class="hljs-string">development,</span> <span class="hljs-string">career,</span> <span class="hljs-string">tools</span>
<span class="hljs-string">?</span> <span class="hljs-string">Release</span> <span class="hljs-string">Date</span> <span class="hljs-string">(format:</span> <span class="hljs-string">yyyy-mm-dd)</span> <span class="hljs-number">2022-01-08</span>
<span class="hljs-string">?</span> <span class="hljs-string">Dry</span> <span class="hljs-string">run</span> <span class="hljs-string">without</span> <span class="hljs-string">creating</span> <span class="hljs-string">files?</span> <span class="hljs-string">(default:</span> <span class="hljs-literal">false</span><span class="hljs-string">)</span> <span class="hljs-literal">Yes</span>

<span class="hljs-attr">Date:</span>
<span class="hljs-number">2022-01-08</span>

<span class="hljs-attr">Slug:</span>
<span class="hljs-string">this-is-a-test-to-see-if-my-script-is-awesome</span>

<span class="hljs-attr">Markdown data:</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">title:</span> <span class="hljs-string">"This Is a Test to See if My Script Is Awesome"</span>
<span class="hljs-attr">categories:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">"development"</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">"career"</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">"tools"</span>
<span class="hljs-attr">cover:</span> <span class="hljs-string">"images/cover.jpg"</span>
<span class="hljs-meta">---</span>

<span class="hljs-comment">## Conclusion</span>

<span class="hljs-string">If</span> <span class="hljs-string">you</span> <span class="hljs-string">liked</span> <span class="hljs-string">this</span> <span class="hljs-string">article,</span> <span class="hljs-string">follow</span> <span class="hljs-string">me</span> <span class="hljs-string">on</span> [<span class="hljs-string">Twitter</span>]<span class="hljs-string">(https://twitter.com/mokkapps)</span> <span class="hljs-string">to</span> <span class="hljs-string">get</span> <span class="hljs-string">notified</span> <span class="hljs-string">about</span> <span class="hljs-string">new</span> <span class="hljs-string">blog</span> <span class="hljs-string">posts</span> <span class="hljs-string">and</span> <span class="hljs-string">more</span> <span class="hljs-string">content</span> <span class="hljs-string">from</span> <span class="hljs-string">me.</span>

<span class="hljs-string">Alternatively</span> <span class="hljs-string">(or</span> <span class="hljs-string">additionally),</span> <span class="hljs-string">you</span> <span class="hljs-string">can</span> <span class="hljs-string">also</span> [<span class="hljs-string">subscribe</span> <span class="hljs-string">to</span> <span class="hljs-string">my</span> <span class="hljs-string">newsletter</span>]<span class="hljs-string">(https://mokkapps.de/newsletter).</span>
</code></pre><p>The script asks for some mandatory information, converts the entered title to title caps, and finally generates the markdown file with the slug name at the correct directory.</p>
<p>Additionally, I have a script to generate a Table of Content (ToC) for a finished article and an image optimization script.</p>
<h2 id="heading-10-open-source-analytics10-open-source-analytics">10. Open Source Analytics<a class="post-section-overview" href="#10-open-source-analytics"></a></h2>
<p>I use <a target="_blank" href="https://github.com/mikecao/umami">Umami</a> with a database hosted on <a target="_blank" href="https://www.digitalocean.com/">Digital Ocean</a>. I send custom events if a visitor, for example, clicks a social link, subscribes to the newsletter or, edits an article on GitHub. These events provide some valuable insights into how many visitors are using the features on my portfolio website.</p>
<p><a target="_blank" href="/static/eb310deccc1f59c7944c167dc410aa05/d9c39/analytics.jpg"><img src="https://www.mokkapps.de/static/eb310deccc1f59c7944c167dc410aa05/15ec7/analytics.jpg" alt="Analytics Events" /></a></p>
<h2 id="heading-conclusionconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>My portfolio website is my favorite digital playground. I love to experiment with different new features and try to provide the best possible experience for visitors.</p>
<p>The source code of my website is <a target="_blank" href="https://github.com/Mokkapps/website">available on GitHub</a>, so feel free to take a closer look if you are interested in implementation details. Leave a comment if you want more information about a specific topic.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[How I Built a Twitter Keyword Monitoring Using a Serverless Node.js Function With AWS Amplify]]></title><description><![CDATA[In this article, I will demonstrate to you how I built a simple serverless Node.js function on AWS that sends me a daily email with a list of tweets that mention me on Twitter.
Recently, I used Twilert and Birdspotter for that purpose, which are spec...]]></description><link>https://blog.mokkapps.de/how-i-built-a-twitter-keyword-monitoring-using-a-serverless-nodejs-function-with-aws-amplify</link><guid isPermaLink="true">https://blog.mokkapps.de/how-i-built-a-twitter-keyword-monitoring-using-a-serverless-nodejs-function-with-aws-amplify</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[Twitter]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[monitoring]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Wed, 15 Dec 2021 09:32:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1639560782792/dBZr3q2lG.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, I will demonstrate to you how I built a simple serverless Node.js function on <a target="_blank" href="https://aws.amazon.com/">AWS</a> that sends me a daily email with a list of tweets that mention me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a>.</p>
<p>Recently, I used <a target="_blank" href="https://twilert.com/">Twilert</a> and <a target="_blank" href="https://birdspotter.net/">Birdspotter</a> for that purpose, which are specialized tools for Twitter keyword monitoring. But their free plans/trials don’t fulfill my simple requirements, so I decided to implement them independently.</p>
<h2 id="heading-prerequisiteslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterprerequisites">Prerequisites<a class="post-section-overview" href="#prerequisites"></a></h2>
<p>I chose <a target="_blank" href="https://www.mokkapps.de/categories/aws">again</a> AWS Amplify to deploy the serverless function to <a target="_blank" href="https://aws.amazon.com/">AWS</a>.</p>
<p>If you don’t already have an AWS account, you’ll need to create one to follow the steps outlined in this article. Please follow <a target="_blank" href="https://portal.aws.amazon.com/billing/signup?redirect_url=https%3A%2F%2Faws.amazon.com%2Fregistration-confirmation#/start">this tutorial</a> to create an account.</p>
<p>Next, you need to install and configure the <a target="_blank" href="https://docs.amplify.aws/start/getting-started/installation/q/integration/js/#install-and-configure-the-amplify-cli">Amplify Command Line Interface (CLI)</a>.</p>
<p>The serverless function will need access to secrets stored in the <a target="_blank" href="https://aws.amazon.com/secrets-manager/">AWS Secret Manager</a>. My article <a target="_blank" href="https://www.mokkapps.de/blog/how-to-use-environment-variables-to-store-secrets-in-aws-amplify-backend/">“How to Use Environment Variables to Store Secrets in AWS Amplify Backend”</a> will guide you through this process.</p>
<h2 id="heading-add-serverless-function-to-awslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreateradd-serverless-function-to-aws">Add Serverless Function to AWS<a class="post-section-overview" href="#add-serverless-function-to-aws"></a></h2>
<p>The first step is to add a new Lambda (serverless) function with the Node.js runtime to the Amplify application.</p>
<p>The function gets invoked on a recurring schedule. In my case, it will be invoked every day at 08:00 PM.</p>
<p>Let’s add the serverless function using the Amplify CLI:</p>
<pre><code class="lang-bash">▶ amplify add <span class="hljs-keyword">function</span>
? Select <span class="hljs-built_in">which</span> capability you want to add: Lambda <span class="hljs-keyword">function</span> (serverless <span class="hljs-keyword">function</span>)
? Provide an AWS Lambda <span class="hljs-keyword">function</span> name: twittersearchfunction
? Choose the runtime that you want to use: NodeJS
? Choose the <span class="hljs-keyword">function</span> template that you want to use: Hello World
? Do you want to configure advanced settings? Yes
? Do you want to access other resources <span class="hljs-keyword">in</span> this project from your Lambda <span class="hljs-keyword">function</span>? No
? Do you want to invoke this <span class="hljs-keyword">function</span> on a recurring schedule? Yes
? At <span class="hljs-built_in">which</span> interval should the <span class="hljs-keyword">function</span> be invoked: Daily
? Select the start time (use arrow keys): 08:00 PM
? Do you want to <span class="hljs-built_in">enable</span> Lambda layers <span class="hljs-keyword">for</span> this <span class="hljs-keyword">function</span>? No
? Do you want to configure environment variables <span class="hljs-keyword">for</span> this <span class="hljs-keyword">function</span>? No
? Do you want to configure secret values this <span class="hljs-keyword">function</span> can access? No
? Do you want to edit the <span class="hljs-built_in">local</span> lambda <span class="hljs-keyword">function</span> now? No
</code></pre>
<h2 id="heading-get-a-list-of-tweets-for-a-specific-twitter-keywordlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterget-a-list-of-tweets-for-a-specific-twitter-keyword">Get a list of tweets for a specific Twitter keyword<a class="post-section-overview" href="#get-a-list-of-tweets-for-a-specific-twitter-keyword"></a></h2>
<p>Now it’s time to write the JavaScript code that returns a list of tweets for a given keyword.</p>
<p>Let’s start by writing the <code>twitter-client.js</code> module. This module uses <a target="_blank" href="https://github.com/FeedHive/twitter-api-client">FeedHive’s Twitter Client</a> to access the <a target="_blank" href="https://developer.twitter.com/en/docs/twitter-api">Twitter API</a>. The first step is to initialize <a target="_blank" href="https://github.com/FeedHive/twitter-api-client">the Twitter API client</a> and trigger the request:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> mokkappsTwitterId = <span class="hljs-number">481186762</span>;
<span class="hljs-keyword">const</span> searchQuery = <span class="hljs-string">'mokkapps'</span>;
<span class="hljs-keyword">const</span> searchResultCount = <span class="hljs-number">100</span>;

<span class="hljs-keyword">const</span> fetchRecentTweets = <span class="hljs-keyword">async</span> secretValues =&gt; {
  <span class="hljs-comment">// Configure Twitter API Client</span>
  <span class="hljs-keyword">const</span> twitterClient = <span class="hljs-keyword">new</span> twitterApiClient.TwitterClient({
    <span class="hljs-attr">apiKey</span>: secretValues.TWITTER_API_KEY,
    <span class="hljs-attr">apiSecret</span>: secretValues.TWITTER_API_KEY_SECRET,
    <span class="hljs-attr">accessToken</span>: secretValues.TWITTER_ACCESS_TOKEN,
    <span class="hljs-attr">accessTokenSecret</span>: secretValues.TWITTER_ACCESS_TOKEN_SECRET,
  });

  <span class="hljs-comment">// Trigger search endpoint: https://github.com/FeedHive/twitter-api-client/blob/main/REFERENCES.md#twitterclienttweetssearchparameters</span>
  <span class="hljs-keyword">const</span> searchResponse = <span class="hljs-keyword">await</span> twitterClient.tweets.search({
    <span class="hljs-attr">q</span>: searchQuery,
    <span class="hljs-attr">count</span>: searchResultCount,
    <span class="hljs-attr">result_type</span>: <span class="hljs-string">'recent'</span>,
  });

  <span class="hljs-comment">// Access statuses from response</span>
  <span class="hljs-keyword">const</span> statuses = searchResponse.statuses;
};
</code></pre>
<p>Next, we want to filter the response into three groups:</p>
<ul>
<li>Tweets: Tweets from the last 24 hours that were not published by my Twitter account and are no replies or retweets</li>
<li>Replies: Tweets from the last 24 hours that were not published by my Twitter account and are replies</li>
<li>Retweets: Tweets from the last 24 hours that were not published by my Twitter account and are retweets</li>
</ul>
<p>Let’s start by the filtering the <code>statuses</code> response for “normal” tweets that are no replies or retweets:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> isTweetedInLast24Hours = <span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> {
  <span class="hljs-keyword">const</span> tweetDate = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(status.created_at);
  <span class="hljs-keyword">const</span> now = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
  <span class="hljs-keyword">const</span> timeDifference = now.getTime() - tweetDate.getTime();
  <span class="hljs-keyword">const</span> daysDifference = timeDifference / (<span class="hljs-number">1000</span> * <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">24</span>);
  <span class="hljs-keyword">return</span> daysDifference &lt;= <span class="hljs-number">1</span>;
};

<span class="hljs-keyword">const</span> fetchRecentTweets = <span class="hljs-keyword">async</span> secretValues =&gt; {
  <span class="hljs-comment">// ...</span>
  <span class="hljs-keyword">const</span> statuses = searchResponse.statuses;

  <span class="hljs-keyword">const</span> tweets = statuses.filter(<span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> { <span class="hljs-keyword">const</span> isNotOwnAccount = status.user.id !== mokkappsTwitterId; <span class="hljs-keyword">const</span> isNoReply = status.in_reply_to_status_id === <span class="hljs-literal">null</span>; <span class="hljs-keyword">const</span> isNoRetweet = status.retweeted_status === <span class="hljs-literal">null</span>; <span class="hljs-keyword">return</span> ( isNotOwnAccount &amp;&amp; isNoReply &amp;&amp; isNoRetweet &amp;&amp; isTweetedInLast24Hours(status) ); });};
</code></pre>
<p>Now we can filter for retweets and replies in a similar way:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> retweets = statuses.filter(<span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> {
  <span class="hljs-keyword">const</span> isNotOwnAccount = status.user.id !== mokkappsTwitterId;
  <span class="hljs-keyword">const</span> isRetweet = status.retweeted_status;
  <span class="hljs-keyword">return</span> isNotOwnAccount &amp;&amp; isRetweet &amp;&amp; isTweetedInLast24Hours(status);
});

<span class="hljs-keyword">const</span> replies = statuses.filter(<span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> {
  <span class="hljs-keyword">const</span> isNotOwnAccount = status.user.id !== mokkappsTwitterId;
  <span class="hljs-keyword">const</span> isReply = status.in_reply_to_status_id !== <span class="hljs-literal">null</span>;
  <span class="hljs-keyword">return</span> isNotOwnAccount &amp;&amp; isReply &amp;&amp; isTweetedInLast24Hours(status);
});
</code></pre>
<p>The last step is to map the results to a very simple HTML structure that will be rendered inside the email body:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> { formatDistance } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'date-fns'</span>);

<span class="hljs-keyword">const</span> mapStatus = <span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> {
  <span class="hljs-keyword">const</span> {
    <span class="hljs-attr">id_str</span>: id,
    created_at,
    in_reply_to_screen_name,
    in_reply_to_status_id_str,
    text,
    retweet_count,
    favorite_count,
    <span class="hljs-attr">user</span>: {
      <span class="hljs-attr">screen_name</span>: user_screen_name,
      followers_count,
      <span class="hljs-attr">created_at</span>: userCreatedAt,
      friends_count,
    },
  } = status;
  <span class="hljs-keyword">const</span> createdAtLocaleString = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(created_at).toLocaleString();
  <span class="hljs-keyword">const</span> url = <span class="hljs-string">`https://twitter.com/<span class="hljs-subst">${user_screen_name}</span>/status/<span class="hljs-subst">${id}</span>`</span>;
  <span class="hljs-keyword">const</span> userUrl = <span class="hljs-string">`https://twitter.com/<span class="hljs-subst">${user_screen_name}</span>`</span>;
  <span class="hljs-keyword">const</span> originalUrl = in_reply_to_screen_name
    ? <span class="hljs-string">`https://twitter.com/<span class="hljs-subst">${in_reply_to_screen_name}</span>/status/<span class="hljs-subst">${in_reply_to_status_id_str}</span>`</span>
    : <span class="hljs-literal">null</span>;
  <span class="hljs-keyword">const</span> userCreatedDateDistance = formatDistance(
    <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
    <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(userCreatedAt)
  );

  <span class="hljs-keyword">return</span> <span class="hljs-string">`
    &lt;div style="margin-bottom: 20px; padding: 10px; border: 1px solid gray; border-radius: 5px;"&gt;
      &lt;h2&gt;From &lt;a href=<span class="hljs-subst">${userUrl}</span>&gt;<span class="hljs-subst">${user_screen_name}</span>&lt;/a&gt; at <span class="hljs-subst">${createdAtLocaleString}</span>&lt;/h2&gt;
      &lt;small&gt;&lt;strong&gt;Followers:&lt;/strong&gt; <span class="hljs-subst">${followers_count}</span>, &lt;strong&gt;Following:&lt;/strong&gt; <span class="hljs-subst">${friends_count}</span>, &lt;strong&gt;Account Created:&lt;/strong&gt; <span class="hljs-subst">${userCreatedDateDistance}</span> ago&lt;/small&gt;
      &lt;h3&gt;<span class="hljs-subst">${text}</span>&lt;/h3&gt;
      &lt;a href=<span class="hljs-subst">${url}</span> style="margin-top: 10px"&gt;Tweet&lt;/a&gt;
      &lt;small style="margin-top: 5px"&gt;(&lt;strong&gt;Likes:&lt;/strong&gt; <span class="hljs-subst">${favorite_count}</span>, &lt;strong&gt;Retweets: <span class="hljs-subst">${retweet_count}</span>)&lt;/strong&gt;&lt;/small&gt;
      <span class="hljs-subst">${
        originalUrl
          ? <span class="hljs-string">`&lt;div style="margin-top: 10px"&gt;&lt;/br&gt;&lt;a href=<span class="hljs-subst">${originalUrl}</span>&gt;Original Tweet&lt;/a&gt;&lt;/div&gt;`</span>
          : <span class="hljs-string">''</span>
      }</span>
    &lt;/div&gt;
    `</span>;
};

<span class="hljs-keyword">const</span> fetchRecentTweets = <span class="hljs-keyword">async</span> secretValues =&gt; {
  <span class="hljs-comment">// ...</span>
  <span class="hljs-keyword">const</span> retweets = statuses
    .filter(<span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> {
      <span class="hljs-keyword">const</span> isNotOwnAccount = status.user.id !== mokkappsTwitterId;
      <span class="hljs-keyword">const</span> isRetweet = status.retweeted_status;
      <span class="hljs-keyword">return</span> isNotOwnAccount &amp;&amp; isRetweet &amp;&amp; isTweetedInLast24Hours(status);
    })
    .map(<span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> mapStatus(status));};
</code></pre>
<p>This is the code for the whole <code>twitter-client.js</code> module:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> twitterApiClient = <span class="hljs-built_in">require</span>(<span class="hljs-string">'twitter-api-client'</span>);
<span class="hljs-keyword">const</span> { formatDistance } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'date-fns'</span>);

<span class="hljs-keyword">const</span> mokkappsTwitterId = <span class="hljs-number">481186762</span>;
<span class="hljs-keyword">const</span> searchQuery = <span class="hljs-string">'mokkapps'</span>;
<span class="hljs-keyword">const</span> searchResultCount = <span class="hljs-number">100</span>;

<span class="hljs-keyword">const</span> mapStatus = <span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> {
  <span class="hljs-keyword">const</span> {
    <span class="hljs-attr">id_str</span>: id,
    created_at,
    in_reply_to_screen_name,
    in_reply_to_status_id_str,
    text,
    retweet_count,
    favorite_count,
    <span class="hljs-attr">user</span>: {
      <span class="hljs-attr">screen_name</span>: user_screen_name,
      followers_count,
      <span class="hljs-attr">created_at</span>: userCreatedAt,
      friends_count,
    },
  } = status;
  <span class="hljs-keyword">const</span> createdAtLocaleString = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(created_at).toLocaleString();
  <span class="hljs-keyword">const</span> url = <span class="hljs-string">`https://twitter.com/<span class="hljs-subst">${user_screen_name}</span>/status/<span class="hljs-subst">${id}</span>`</span>;
  <span class="hljs-keyword">const</span> userUrl = <span class="hljs-string">`https://twitter.com/<span class="hljs-subst">${user_screen_name}</span>`</span>;
  <span class="hljs-keyword">const</span> originalUrl = in_reply_to_screen_name
    ? <span class="hljs-string">`https://twitter.com/<span class="hljs-subst">${in_reply_to_screen_name}</span>/status/<span class="hljs-subst">${in_reply_to_status_id_str}</span>`</span>
    : <span class="hljs-literal">null</span>;
  <span class="hljs-keyword">const</span> userCreatedDateDistance = formatDistance(
    <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
    <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(userCreatedAt)
  );

  <span class="hljs-keyword">return</span> <span class="hljs-string">`
    &lt;div style="margin-bottom: 20px; padding: 10px; border: 1px solid gray; border-radius: 5px;"&gt;
      &lt;h2&gt;From &lt;a href=<span class="hljs-subst">${userUrl}</span>&gt;<span class="hljs-subst">${user_screen_name}</span>&lt;/a&gt; at <span class="hljs-subst">${createdAtLocaleString}</span>&lt;/h2&gt;
      &lt;small&gt;&lt;strong&gt;Followers:&lt;/strong&gt; <span class="hljs-subst">${followers_count}</span>, &lt;strong&gt;Following:&lt;/strong&gt; <span class="hljs-subst">${friends_count}</span>, &lt;strong&gt;Account Created:&lt;/strong&gt; <span class="hljs-subst">${userCreatedDateDistance}</span> ago&lt;/small&gt;
      &lt;h3&gt;<span class="hljs-subst">${text}</span>&lt;/h3&gt;
      &lt;a href=<span class="hljs-subst">${url}</span> style="margin-top: 10px"&gt;Tweet&lt;/a&gt;
      &lt;small style="margin-top: 5px"&gt;(&lt;strong&gt;Likes:&lt;/strong&gt; <span class="hljs-subst">${favorite_count}</span>, &lt;strong&gt;Retweets: <span class="hljs-subst">${retweet_count}</span>)&lt;/strong&gt;&lt;/small&gt;
      <span class="hljs-subst">${
        originalUrl
          ? <span class="hljs-string">`&lt;div style="margin-top: 10px"&gt;&lt;/br&gt;&lt;a href=<span class="hljs-subst">${originalUrl}</span>&gt;Original Tweet&lt;/a&gt;&lt;/div&gt;`</span>
          : <span class="hljs-string">''</span>
      }</span>
    &lt;/div&gt;
    `</span>;
};

<span class="hljs-keyword">const</span> isTweetedInLast24Hours = <span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> {
  <span class="hljs-keyword">const</span> tweetDate = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(status.created_at);
  <span class="hljs-keyword">const</span> now = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
  <span class="hljs-keyword">const</span> timeDifference = now.getTime() - tweetDate.getTime();
  <span class="hljs-keyword">const</span> daysDifference = timeDifference / (<span class="hljs-number">1000</span> * <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">24</span>);
  <span class="hljs-keyword">return</span> daysDifference &lt;= <span class="hljs-number">1</span>;
};

<span class="hljs-keyword">const</span> fetchRecentTweets = <span class="hljs-keyword">async</span> secretValues =&gt; {
  <span class="hljs-keyword">const</span> twitterClient = <span class="hljs-keyword">new</span> twitterApiClient.TwitterClient({
    <span class="hljs-attr">apiKey</span>: secretValues.TWITTER_API_KEY,
    <span class="hljs-attr">apiSecret</span>: secretValues.TWITTER_API_KEY_SECRET,
    <span class="hljs-attr">accessToken</span>: secretValues.TWITTER_ACCESS_TOKEN,
    <span class="hljs-attr">accessTokenSecret</span>: secretValues.TWITTER_ACCESS_TOKEN_SECRET,
  });

  <span class="hljs-keyword">const</span> searchResponse = <span class="hljs-keyword">await</span> twitterClient.tweets.search({
    <span class="hljs-attr">q</span>: searchQuery,
    <span class="hljs-attr">count</span>: searchResultCount,
    <span class="hljs-attr">result_type</span>: <span class="hljs-string">'recent'</span>,
  });

  <span class="hljs-keyword">const</span> statuses = searchResponse.statuses;

  <span class="hljs-keyword">const</span> tweets = statuses
    .filter(<span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> {
      <span class="hljs-keyword">const</span> isNotOwnAccount = status.user.id !== mokkappsTwitterId;
      <span class="hljs-keyword">const</span> isNoReply = status.in_reply_to_status_id === <span class="hljs-literal">null</span>;
      <span class="hljs-keyword">const</span> isNoRetweet = status.retweeted_status === <span class="hljs-literal">null</span>;
      <span class="hljs-keyword">return</span> (
        isNotOwnAccount &amp;&amp;
        isNoReply &amp;&amp;
        isNoRetweet &amp;&amp;
        isTweetedInLast24Hours(status)
      );
    })
    .map(<span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> mapStatus(status));

  <span class="hljs-keyword">const</span> retweets = statuses
    .filter(<span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> {
      <span class="hljs-keyword">const</span> isNotOwnAccount = status.user.id !== mokkappsTwitterId;
      <span class="hljs-keyword">const</span> isRetweet = status.retweeted_status;
      <span class="hljs-keyword">return</span> isNotOwnAccount &amp;&amp; isRetweet &amp;&amp; isTweetedInLast24Hours(status);
    })
    .map(<span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> mapStatus(status));

  <span class="hljs-keyword">const</span> replies = statuses
    .filter(<span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> {
      <span class="hljs-keyword">const</span> isNotOwnAccount = status.user.id !== mokkappsTwitterId;
      <span class="hljs-keyword">const</span> isReply = status.in_reply_to_status_id !== <span class="hljs-literal">null</span>;
      <span class="hljs-keyword">return</span> isNotOwnAccount &amp;&amp; isReply &amp;&amp; isTweetedInLast24Hours(status);
    })
    .map(<span class="hljs-function"><span class="hljs-params">status</span> =&gt;</span> mapStatus(status));

  <span class="hljs-keyword">return</span> {
    tweets,
    retweets,
    replies,
  };
};

<span class="hljs-built_in">module</span>.exports = fetchRecentTweets;
</code></pre>
<h2 id="heading-serverless-function-codelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterserverless-function-code">Serverless function code<a class="post-section-overview" href="#serverless-function-code"></a></h2>
<p>We can now use the <code>twitter-client.js</code> in our serverless function:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> AWS = <span class="hljs-built_in">require</span>(<span class="hljs-string">'aws-sdk'</span>);
<span class="hljs-keyword">const</span> nodemailer = <span class="hljs-built_in">require</span>(<span class="hljs-string">'nodemailer'</span>);
<span class="hljs-keyword">const</span> fetchRecentTweets = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./twitter-client'</span>);

<span class="hljs-keyword">const</span> secretsManager = <span class="hljs-keyword">new</span> AWS.SecretsManager();
<span class="hljs-keyword">const</span> responseHeaders = {
  <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>
};

<span class="hljs-built_in">exports</span>.handler = <span class="hljs-keyword">async</span> event =&gt; {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`👷 Function is ready to search for tweets`</span>);

  <span class="hljs-keyword">const</span> secretData = <span class="hljs-keyword">await</span> secretsManager
    .getSecretValue({ <span class="hljs-attr">SecretId</span>: <span class="hljs-string">'YOUR_SECRET_ID'</span> })
    .promise();
  <span class="hljs-keyword">const</span> secretValues = <span class="hljs-built_in">JSON</span>.parse(secretData.SecretString);

  <span class="hljs-keyword">const</span> transporter = nodemailer.createTransport({
    <span class="hljs-attr">service</span>: secretValues.MAIL_HOST,
    <span class="hljs-attr">auth</span>: {
      <span class="hljs-attr">user</span>: secretValues.MAIL_USER,
      <span class="hljs-attr">pass</span>: secretValues.MAIL_PW,
    },
  });

  <span class="hljs-keyword">const</span> defaultMailOptions = {
    <span class="hljs-attr">from</span>: secretValues.MAIL_USER,
    <span class="hljs-attr">to</span>: secretValues.MAIL_SUCCESS,
    <span class="hljs-attr">subject</span>: <span class="hljs-string">`[Mokkapps API] Twitter Search Results`</span>,
  };

  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// Fetch recent tweets</span>
    <span class="hljs-keyword">const</span> { tweets, replies, retweets } = <span class="hljs-keyword">await</span> fetchRecentTweets(secretValues);

    <span class="hljs-comment">// Skip sending email if we have no results</span>
    <span class="hljs-keyword">if</span> (tweets.length === <span class="hljs-number">0</span> &amp;&amp; replies.length === <span class="hljs-number">0</span> &amp;&amp; retweets.length === <span class="hljs-number">0</span>) {
      <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
        <span class="hljs-attr">headers</span>: responseHeaders,
        <span class="hljs-attr">body</span>: [],
      };
    }

    <span class="hljs-comment">// Send email</span>
    <span class="hljs-keyword">await</span> transporter.sendMail({
      ...defaultMailOptions,
      <span class="hljs-attr">html</span>: <span class="hljs-string">`
        &lt;h1&gt;Tweets that mentioned "mokkapps" in the last 24 hours&lt;/h1&gt;
        <span class="hljs-subst">${tweets.length === <span class="hljs-number">0</span> ? <span class="hljs-string">'&lt;p&gt;No results&lt;/p&gt;'</span> : tweets.join(<span class="hljs-string">''</span>)}</span>
        &lt;h1&gt;Replies that mentioned "mokkapps" in the last 24 hours&lt;/h1&gt;
        <span class="hljs-subst">${replies.length === <span class="hljs-number">0</span> ? <span class="hljs-string">'&lt;p&gt;No results&lt;/p&gt;'</span> : replies.join(<span class="hljs-string">''</span>)}</span>
        &lt;h1&gt;Retweets that mentioned "mokkapps" in the last 24 hours&lt;/h1&gt;
        <span class="hljs-subst">${retweets.length === <span class="hljs-number">0</span> ? <span class="hljs-string">'&lt;p&gt;No results&lt;/p&gt;'</span> : retweets.join(<span class="hljs-string">''</span>)}</span>
      `</span>,
    });

    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
      <span class="hljs-attr">headers</span>: responseHeaders,
      <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({ tweets, replies, retweets }),
    };
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'☠ Twitter Search Function Error:'</span>, e);
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
      <span class="hljs-attr">headers</span>: responseHeaders,
      <span class="hljs-attr">body</span>: e.message ? e.message : <span class="hljs-built_in">JSON</span>.stringify(e),
    };
  }
};
</code></pre>
<p>At this point, we can publish our function by running:</p>
<pre><code class="lang-bash">amplify push
</code></pre>
<p>If we successfully pushed the function to AWS, we can manually invoke the function in <a target="_blank" href="https://aws.amazon.com/lambda/">AWS Lamba</a> by clicking the “Test” button:</p>
<p><a target="_blank" href="/static/e8819c09fcd65edff9111886867d04b7/d9c39/aws-lambda-test.jpg"><img src="https://www.mokkapps.de/static/e8819c09fcd65edff9111886867d04b7/15ec7/aws-lambda-test.jpg" alt="AWS Lambda Function Test" /></a>
<em>AWS Lambda Function Test</em></p>
<p>The serverless function should then send an email with a list of tweets if someone mentioned the monitored keyword in the last 24 hours:</p>
<p><a target="_blank" href="/static/71b70855d75a8a8a6bc36f85ee10c89c/d9c39/twitter-keyword-monitoring-email.jpg"><img src="https://www.mokkapps.de/static/71b70855d75a8a8a6bc36f85ee10c89c/15ec7/twitter-keyword-monitoring-email.jpg" alt="Email sent from serverless Node.js function" /></a>
<em>Email sent from serverless Node.js function</em></p>
<h2 id="heading-conclusionlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>I had a lot of fun building this simple serverless function to monitor keywords on Twitter.</p>
<p>Serverless functions are a perfect choice for such a monitoring tool, as we only have to pay for the execution time of the serverless function.</p>
<p>What do you think about my solution? Leave a comment and tell me how you monitor your Twitter keywords.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Document & Test Vue 3 Components With Storybook]]></title><description><![CDATA[Storybook is my tool of choice for UI component documentation. Vue.js is very well supported in the Storybook ecosystem and has first-class integrations with Vuetify and NuxtJS. It also has official support for Vue 3, the latest major installment of ...]]></description><link>https://blog.mokkapps.de/document-and-test-vue-3-components-with-storybook</link><guid isPermaLink="true">https://blog.mokkapps.de/document-and-test-vue-3-components-with-storybook</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[vue]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[frontend]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Mon, 15 Nov 2021 12:36:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1636979855718/piNlWV9sJ.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://storybook.js.org/">Storybook</a> is my tool of choice for UI component documentation. Vue.js is very well supported in the Storybook ecosystem and has first-class integrations with <a target="_blank" href="https://github.com/vuetifyjs/vue-cli-plugins/tree/master/packages/vue-cli-plugin-vuetify-storybook">Vuetify</a> and <a target="_blank" href="https://storybook.nuxtjs.org/">NuxtJS</a>. It also has official support for <a target="_blank" href="https://v3.vuejs.org/">Vue 3</a>, the latest major installment of Vue.js.</p>
<p>This article will demonstrate how you can set up Storybook with zero-config and built-in TypeScript support, auto-generate controls &amp; documentation, and perform automated snapshot tests for your Vue components.</p>
<h2 id="why-storybooklesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterwhy-storybook">Why Storybook?<a class="post-section-overview" href="#why-storybook"></a></h2>
<p>We have components that can have many props, states, slots, etc., which influences its visual representation and more.</p>
<p>This circumstance causes some typical problems for any front-end developer:</p>
<ul>
<li>How can I create documentation for my component that doesn’t get outdated?</li>
<li>How can I get an overview of all different states and kinds of my component?</li>
<li>How can I guarantee that my changes don’t influence other states and kinds?</li>
<li>How can I show the current implementation to non-developer team members?</li>
</ul>
<p>Storybook will help us here.</p>
<h2 id="storybook-setuplesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterstorybook-setup">Storybook Setup<a class="post-section-overview" href="#storybook-setup"></a></h2>
<p>First, we need to create a Vue 3 application. We’ll use <a target="_blank" href="https://vitejs.dev/">Vite</a>, a new build tool from <a target="_blank" href="https://twitter.com/youyuxi">Evan You</a>, the creator of Vue.js:</p>
<pre><code class="lang-bash">npm init vite@latest
</code></pre>
<p>Setting up Storybook in an existing Vue 3 project can be done with zero configuration:</p>
<pre><code class="lang-bash">npx sb init
</code></pre>
<p>This command installs Storybook with its dependencies, configures the Storybook instance, and generates some demo components and stories which are located at <code>src/stories</code>:</p>
<p><a target="_blank" href="/static/1a0cbe0496294c6669d21a4d3638aaf5/00e65/storybook-init-folder.png"><img src="https://www.mokkapps.de/static/1a0cbe0496294c6669d21a4d3638aaf5/00e65/storybook-init-folder.png" alt="Storybook Vue 3 Generated Files" /></a>
<em>Storybook Vue 3 Generated Files</em></p>
<p>We can now run the following command, which starts a local development server for Storybook and automatically opens it in a new browser tab:</p>
<pre><code class="lang-bash">npm run storybook
</code></pre>
<p><a target="_blank" href="/static/dca97b349ffe5fc1c352cca3c6ffd6ef/d61c2/storybook-demo.png"><img src="https://www.mokkapps.de/static/dca97b349ffe5fc1c352cca3c6ffd6ef/1e043/storybook-demo.png" alt="Storybook Vue 3 Demo" /></a>
<em>Storybook Vue 3 Demo</em></p>
<p>These generated Vue components and stories are good examples of how to write Vue 3 stories. I want to show you some advanced documentation examples using a custom component.</p>
<h2 id="custom-component-demolesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatercustom-component-demo">Custom Component Demo<a class="post-section-overview" href="#custom-component-demo"></a></h2>
<p>I created a <code>Counter.vue</code> demo component to demonstrate the Storybook integration for this article. The source code is available at <a target="_blank" href="https://github.com/Mokkapps/vue-3-storybook-demo">GitHub</a>.</p>
<p>The component provides basic counter functionality, has two different visual variants and two slots for custom content.</p>
<p>Let’s take a look at the component’s code:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>{{ label }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
  <span class="hljs-comment">&lt;!-- @slot Slot to show content below label --&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">slot</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"sub-label"</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"container"</span> <span class="hljs-attr">:class</span>=<span class="hljs-string">"variant"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"increment()"</span>&gt;</span>+<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"value"</span>&gt;</span>{{ count }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"decrement()"</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-comment">&lt;!-- @slot Default slot to show any content below the counter --&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">slot</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref, watch, PropType } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;
<span class="hljs-keyword">import</span> { Variant } <span class="hljs-keyword">from</span> <span class="hljs-string">'./types'</span>;

<span class="hljs-comment">/** * This is my amazing counter component * * It can increment and decrement! */</span><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">props</span>: {
    <span class="hljs-comment">/** * The initial value for the counter */</span> initialValue: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">Number</span>,
      <span class="hljs-attr">default</span>: <span class="hljs-number">0</span>,
    },
    <span class="hljs-comment">/** * Text shown above the counter */</span> label: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
      <span class="hljs-attr">default</span>: <span class="hljs-string">'Counter'</span>,
    },
    <span class="hljs-comment">/** * If true, the counter can show negative numbers */</span> allowNegativeValues: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">Boolean</span>,
      <span class="hljs-attr">default</span>: <span class="hljs-literal">false</span>,
    },
    <span class="hljs-comment">/** * Defines the visual appearance of the counter */</span> variant: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span> <span class="hljs-keyword">as</span> PropType&lt;Variant&gt;,
      <span class="hljs-keyword">default</span>: Variant.Default,
    },
  },
  <span class="hljs-attr">emits</span>: [<span class="hljs-string">'counter-update'</span>],
  setup(props, context) {
    <span class="hljs-keyword">const</span> count = ref(props.initialValue);

    <span class="hljs-keyword">const</span> increment = <span class="hljs-function">() =&gt;</span> {
      count.value += <span class="hljs-number">1</span>;
    };

    <span class="hljs-keyword">const</span> decrement = <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">const</span> newValue = count.value - <span class="hljs-number">1</span>;
      <span class="hljs-keyword">if</span> (newValue &lt; <span class="hljs-number">0</span> &amp;&amp; !props.allowNegativeValues) {
        count.value = <span class="hljs-number">0</span>;
      } <span class="hljs-keyword">else</span> {
        count.value -= <span class="hljs-number">1</span>;
      }
    };

    watch(count, <span class="hljs-function"><span class="hljs-params">value</span> =&gt;</span> {
      context.emit(<span class="hljs-string">'counter-update'</span>, value);
    });

    <span class="hljs-keyword">return</span> {
      count,
      increment,
      decrement,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
</code></pre>
<p>In the above code, you can see that I’ve annotated the Vue component with <a target="_blank" href="https://jsdoc.app/">JSDoc</a> comments. Storybook converts them into living documentation alongside our stories.</p>
<p>Unfortunately, I found no way to add JSDoc comments to the <code>counter-update</code> event. I think it is currently not supported in <a target="_blank" href="https://github.com/vue-styleguidist/vue-styleguidist/tree/dev/packages/vue-docgen-api">vue-docgen-api</a>, which Storybook uses under the hood to extract code comments into descriptions. Leave a comment if you know a way how to document events in Vue 3.</p>
<p>Storybook uses so-called <a target="_blank" href="https://storybook.js.org/docs/react/get-started/whats-a-story">stories</a>:</p>
<blockquote>
<p>A story captures the rendered state of a UI component. Developers write multiple stories per component that describe all the “interesting” states a component can support.</p>
</blockquote>
<p>A component’s stories are defined in a story file that lives alongside the component file. The story file is for development-only, it won’t be included in your production bundle.</p>
<p>Now, let’s take a look at the code of our <code>Counter.stories.ts</code>:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> Counter <span class="hljs-keyword">from</span> <span class="hljs-string">'./Counter.vue'</span>;
<span class="hljs-keyword">import</span> { Variant } <span class="hljs-keyword">from</span> <span class="hljs-string">'./types'</span>;

<span class="hljs-comment">//👇 This default export determines where your story goes in the story list</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  title: <span class="hljs-string">'Counter'</span>,
  component: Counter,
  <span class="hljs-comment">//👇 Creates specific argTypes with options</span>
  argTypes: {
    variant: {
      options: Variant,
    },
  },
};

<span class="hljs-comment">//👇 We create a “template” of how args map to rendering</span>
<span class="hljs-keyword">const</span> Template = <span class="hljs-function"><span class="hljs-params">args</span> =&gt;</span> ({
  components: { Counter },
  setup() {
    <span class="hljs-comment">//👇 The args will now be passed down to the template</span>
    <span class="hljs-keyword">return</span> { args };
  },
  template: <span class="hljs-string">'&lt;Counter v-bind="args"&gt;{{ args.slotContent }}&lt;/Counter&gt;'</span>,
});

<span class="hljs-comment">//👇 Each story then reuses that template</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Default = Template.bind({});
Default.args = {
  label: <span class="hljs-string">'Default'</span>,
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Colored = Template.bind({});
Colored.args = {
  label: <span class="hljs-string">'Colored'</span>,
  variant: Variant.Colored,
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> NegativeValues = Template.bind({});
NegativeValues.args = {
  allowNegativeValues: <span class="hljs-literal">true</span>,
  initialValue: <span class="hljs-number">-1</span>,
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Slot = Template.bind({});
Slot.args = {
  slotContent: <span class="hljs-string">'SLOT CONTENT'</span>,
};
</code></pre>
<p>This code is written in <a target="_blank" href="https://storybook.js.org/docs/vue/writing-stories/introduction">Component Story Format</a> and generates four stories:</p>
<ul>
<li>Default: The counter component in its default state</li>
<li>Colored: The counter component in the colored variation</li>
<li>NegativeValue: The counter component that allows negative values</li>
<li>Slot: The counter component with a slot content</li>
</ul>
<p>Let’s take a look at our living documentation in Storybook:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1636979853027/XI7B38it13.gif" alt="Storybook Demo Running" /></p>
<p>As already mentioned, Storybook converts the JSDoc comments from our code snippet above into documentation, shown in the following picture:</p>
<p><a target="_blank" href="/static/81d7a8dc0cae748f61309cf144ef88c8/d61c2/storybook-custom-demo-docs.png"><img src="https://www.mokkapps.de/static/81d7a8dc0cae748f61309cf144ef88c8/1e043/storybook-custom-demo-docs.png" alt="Storybook Generated Docs" /></a>
<em>Storybook Generated Docs</em></p>
<h2 id="testinglesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatertesting">Testing<a class="post-section-overview" href="#testing"></a></h2>
<p>Now that we have our living documentation in Storybook run tests against them.</p>
<h3 id="jest-setuplesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterjest-setup">Jest Setup<a class="post-section-overview" href="#jest-setup"></a></h3>
<p>I chose <a target="_blank" href="https://jestjs.io/">Jest</a> as the test runner. It has a fast &amp; straightforward setup process and includes a test runner, an assertion library, and a DOM implementation to mount our Vue components.</p>
<p>To install Jest in our existing Vue 3 + Vite project, we need to run the following command:</p>
<pre><code class="lang-bash">npm install jest @types/jest ts-jest vue-jest@next @vue/test-utils@next --save-dev
</code></pre>
<p>Then we need to create a <code>jest.config.js</code> config file in the root directory:</p>
<pre><code class="lang-js"><span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">moduleFileExtensions</span>: [<span class="hljs-string">'js'</span>, <span class="hljs-string">'ts'</span>, <span class="hljs-string">'json'</span>, <span class="hljs-string">'vue'</span>],
  <span class="hljs-attr">transform</span>: {
    <span class="hljs-string">'^.+\\.ts$'</span>: <span class="hljs-string">'ts-jest'</span>,
    <span class="hljs-string">'^.+\\.vue$'</span>: <span class="hljs-string">'vue-jest'</span>,
  },
  <span class="hljs-attr">collectCoverage</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">collectCoverageFrom</span>: [<span class="hljs-string">'/src/**/*.vue'</span>],
};
</code></pre>
<p>The next step is to add a script that executes the tests in our <code>package.json</code>:</p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
  <span class="hljs-attr">"test"</span>: <span class="hljs-string">"jest src"</span>
}
</code></pre>
<h3 id="unit-testing-with-storybooklesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterunit-testing-with-storybook">Unit testing with Storybook<a class="post-section-overview" href="#unit-testing-with-storybook"></a></h3>
<p>Unit tests help verify functional aspects of components. They prove that the output of a component remains the same given a fixed input.</p>
<p>Let’s take a look at a simple unit test for our Storybook story:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { mount } <span class="hljs-keyword">from</span> <span class="hljs-string">'@vue/test-utils'</span>;

<span class="hljs-keyword">import</span> Counter <span class="hljs-keyword">from</span> <span class="hljs-string">'./Counter.vue'</span>;

<span class="hljs-comment">//👇 Imports a specific story for the test</span>
<span class="hljs-keyword">import</span> { Colored, Default } <span class="hljs-keyword">from</span> <span class="hljs-string">'./Counter.stories'</span>;

it(<span class="hljs-string">'renders default button'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> wrapper = mount(Counter, {
    propsData: Default.args,
  });
  expect(wrapper.find(<span class="hljs-string">'.container'</span>).classes()).toContain(<span class="hljs-string">'default'</span>);
});

it(<span class="hljs-string">'renders colored button'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> wrapper = mount(Counter, {
    propsData: Colored.args,
  });
  expect(wrapper.find(<span class="hljs-string">'.container'</span>).classes()).toContain(<span class="hljs-string">'colored'</span>);
});
</code></pre>
<p>We wrote two exemplary unit tests Jest executes against our Storybook story <code>Counter.stories.ts</code>:</p>
<ul>
<li><code>renders default button</code>: asserts that the component container contains the CSS class <code>default</code></li>
<li><code>renders colored button</code>: asserts that the component container contains the CSS class <code>colored</code></li>
</ul>
<p>The test result looks like this:</p>
<pre><code class="lang-bash"> PASS src/components/Counter.test.ts
  ✓ renders default button (25 ms)
  ✓ renders colored button (4 ms)

----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line <span class="hljs-comment">#s</span>
----------|---------|----------|---------|---------|-------------------
All files | 0 | 0 | 0 | 0 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.674 s, estimated 4 s
</code></pre>
<h2 id="snapshot-testinglesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatersnapshot-testing">Snapshot Testing<a class="post-section-overview" href="#snapshot-testing"></a></h2>
<p>Snapshot tests compare the rendered markup of every story against known baselines. It’s an easy way to identify markup changes that trigger rendering errors and warnings.</p>
<p>A snapshot test renders the markup of our story, takes a snapshot, and compares it to a reference snapshot file stored alongside the test.</p>
<p>The test case will fail if the two snapshots do not match. There are two typical causes why a snapshot test fails:</p>
<ul>
<li>The change is expected</li>
<li>The reference snapshot needs to be updated</li>
</ul>
<p>We can use <a target="_blank" href="https://jestjs.io/docs/snapshot-testing">Jest Snapshot Testing</a> as Jest library for snapshot tests.</p>
<p>Let’s install it by running the following command:</p>
<pre><code class="lang-bash">npm install --save-dev jest-serializer-vue
</code></pre>
<p>Next, we need to add it as <code>snapshotSerializers</code> to our <code>jest.config.js</code> config file:</p>
<pre><code class="lang-js"><span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">moduleFileExtensions</span>: [<span class="hljs-string">'js'</span>, <span class="hljs-string">'ts'</span>, <span class="hljs-string">'json'</span>, <span class="hljs-string">'vue'</span>],
  <span class="hljs-attr">transform</span>: {
    <span class="hljs-string">'^.+\\.ts$'</span>: <span class="hljs-string">'ts-jest'</span>,
    <span class="hljs-string">'^.+\\.vue$'</span>: <span class="hljs-string">'vue-jest'</span>,
  },
  <span class="hljs-attr">collectCoverage</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">collectCoverageFrom</span>: [<span class="hljs-string">'/src/**/*.vue'</span>],
  <span class="hljs-attr">snapshotSerializers</span>: [<span class="hljs-string">'jest-serializer-vue'</span>],};
</code></pre>
<p>Finally, we can write a snapshot test for Storybook story:</p>
<pre><code class="lang-ts">it(<span class="hljs-string">'renders snapshot'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> wrapper = mount(Counter, {
    propsData: Colored.args,
  });
  expect(wrapper.element).toMatchSnapshot();
});
</code></pre>
<p>If we now run our tests, we get the following result:</p>
<pre><code class="lang-bash">&gt; vite-vue-typescript-starter@0.0.0 <span class="hljs-built_in">test</span>
&gt; jest src

 PASS src/components/Counter.test.ts
  ✓ renders default button (27 ms)
  ✓ renders colored button (4 ms)
  ✓ renders snapshot (6 ms)

----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line <span class="hljs-comment">#s</span>
----------|---------|----------|---------|---------|-------------------
All files | 0 | 0 | 0 | 0 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 1 passed, 1 total
Time: 1.399 s, estimated 2 s
</code></pre>
<p>The test run generates snapshot reference files that are located at <code>src/components/ __snapshots__</code>.</p>
<h2 id="conclusionlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>Storybook is a fantastic tool to create living documentation for components. If you keep the story files next to your component’s source code, the chances are high that the story gets updated if you modify the component.</p>
<p>Storybook has first-class support for Vue 3, and it works very well. If you want more information about Vue and Storybook, you should look at the <a target="_blank" href="https://storybook.js.org/docs/vue/get-started/introduction">official Storybook documentation</a>.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Why I Love Vue 3's Composition API]]></title><description><![CDATA[Vue 3 introduced the Composition API to provide a better way to collocate code related to the same logical concern. In this article, I want to tell you why I love this new way of writing Vue components.
First, I will show you how you can build compon...]]></description><link>https://blog.mokkapps.de/why-i-love-vue-3s-composition-api</link><guid isPermaLink="true">https://blog.mokkapps.de/why-i-love-vue-3s-composition-api</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[vue]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[frontend]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Tue, 02 Nov 2021 09:48:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1635846571090/qSfEFeZr3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://v3.vuejs.org/">Vue 3</a> introduced the <a target="_blank" href="https://v3.vuejs.org/guide/composition-api-introduction.html">Composition API</a> to provide a better way to collocate code related to the same logical concern. In this article, I want to tell you why I love this new way of writing Vue components.</p>
<p>First, I will show you how you can build components using Vue 2, and then I will show you the same component implemented using Composition API. I’ll explain some of the Composition API basics and why I prefer Composition API for building components.</p>
<p>For this article, I created a <a target="_blank" href="https://stackblitz.com/edit/vue-3-composition-api-demo?file=src/App.vue">Stackblitz Vue 3 demo application</a> which includes all the components that I’ll showcase in this article:</p>
<p><iframe width="100%" height="500" src="https://stackblitz.com/edit/vue-3-composition-api-demo?file=src/App.vue&amp;embed=1"></iframe>
<br /></p>
<p>The source code is also available on <a target="_blank" href="https://github.com/Mokkapps/vue-3-composition-api-demo/">GitHub</a>.</p>
<h2 id="options-apilesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreateroptions-api">Options API<a class="post-section-overview" href="#options-api"></a></h2>
<p>First, let’s look at how we build components in Vue 2 without the Composition API.</p>
<p>In Vue 2 we build components using the Options API by filling (option) properties like methods, data, computed, etc. An example component could look like this:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
  data () {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-comment">// Properties for data, filtering, sorting and paging</span>
    }
  },
  <span class="hljs-attr">methods</span>: {
    <span class="hljs-comment">// Methods for data, filtering, sorting and paging</span>
  },
  <span class="hljs-attr">computed</span>: {
    <span class="hljs-comment">// Values for data, filtering, sorting and paging</span>
  }
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>As you can see, Options API has a significant drawback: The logical concerns (filtering, sorting, etc.) are not grouped but split between the different options of the Options API. Such fragmentation is what makes it challenging to understand and maintain complex Vue components.</p>
<p>Let’s start by looking at <a target="_blank" href="https://github.com/Mokkapps/vue-3-composition-api-demo/blob/master/src/components/CounterOptionsApi.vue">CounterOptionsApi.vue</a>, the Options API counter component:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Counter Options API<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Count: {{ count }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>2^Count: {{ countPow }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"increment()"</span>&gt;</span>Increase Count<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"incrementBy(5)"</span>&gt;</span>Increase Count by 5<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"decrement()"</span>&gt;</span>Decrease Count<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">props</span>: {
    <span class="hljs-attr">initialValue</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">Number</span>,
      <span class="hljs-attr">default</span>: <span class="hljs-number">0</span>,
    },
  },
  <span class="hljs-attr">emits</span>: [<span class="hljs-string">'counter-update'</span>],
  <span class="hljs-attr">data</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">count</span>: <span class="hljs-built_in">this</span>.initialValue,
    };
  },
  <span class="hljs-attr">watch</span>: {
    <span class="hljs-attr">count</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">newCount</span>) </span>{
      <span class="hljs-built_in">this</span>.$emit(<span class="hljs-string">'counter-update'</span>, newCount);
    },
  },
  <span class="hljs-attr">computed</span>: {
    <span class="hljs-attr">countPow</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.count * <span class="hljs-built_in">this</span>.count;
    },
  },
  <span class="hljs-attr">methods</span>: {
    increment() {
      <span class="hljs-built_in">this</span>.count++;
    },
    decrement() {
      <span class="hljs-built_in">this</span>.count--;
    },
    incrementBy(count) {
      <span class="hljs-built_in">this</span>.count += count;
    },
  },
  <span class="hljs-attr">mounted</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Options API counter mounted'</span>);
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>This simple counter component includes multiple essential Vue functionalities:</p>
<ul>
<li>We use a <code>count</code> data property that uses the <code>initialValue</code> property as its initial value.</li>
<li><code>countPow</code> as computed property which calculates the power of the <code>count</code> value.</li>
<li>A watcher that emits the <code>counter-update</code> event if the <code>count</code> value has changed.</li>
<li>Multiple methods to modify the <code>count</code> value.</li>
<li>A <code>console.log</code> message that is written if the <a target="_blank" href="https://vuejs.org/v2/api/#mounted">mounted lifecycle hook</a> was triggered.</li>
</ul>
<p>If you are not familiar with the Vue 2 features mentioned above, you should first read the <a target="_blank" href="https://vuejs.org/v2/guide/">official Vue 2 documentation</a> before you continue reading this article.</p>
<h2 id="composition-apilesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatercomposition-api">Composition API<a class="post-section-overview" href="#composition-api"></a></h2>
<p>Since Vue 3 we can <strong>additionally</strong> use <a target="_blank" href="https://v3.vuejs.org/guide/composition-api-introduction.html#why-composition-api">Composition API</a> to build Vue components.</p>
<p>ℹ️ Composition API is fully optional, and we can still use Options API in Vue 3.</p>
<p>In my <a target="_blank" href="https://stackblitz.com/edit/vue-3-composition-api-demo?file=src/App.vue">demo application</a> I use the same template for all Vue components, so let’s focus on the <code>&lt;script&gt;</code> part of the <a target="_blank" href="https://github.com/Mokkapps/vue-3-composition-api-demo/blob/master/src/components/CounterCompositionApi.vue">CounterCompositionApi.vue</a> component:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref, onMounted, computed, watch } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">props</span>: {
    <span class="hljs-attr">initialValue</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">Number</span>,
      <span class="hljs-attr">default</span>: <span class="hljs-number">0</span>,
    },
  },
  <span class="hljs-attr">emits</span>: [<span class="hljs-string">'counter-update'</span>],
  setup(props, context) {
    <span class="hljs-keyword">const</span> count = ref(props.initialValue);

    <span class="hljs-keyword">const</span> increment = <span class="hljs-function">() =&gt;</span> {
      count.value += <span class="hljs-number">1</span>;
    };
    <span class="hljs-keyword">const</span> decrement = <span class="hljs-function">() =&gt;</span> {
      count.value -= <span class="hljs-number">1</span>;
    };
    <span class="hljs-keyword">const</span> incrementBy = <span class="hljs-function">(<span class="hljs-params">value: number</span>) =&gt;</span> {
      count.value += value;
    };

    <span class="hljs-keyword">const</span> countPow = computed(<span class="hljs-function">() =&gt;</span> count.value * count.value);

    watch(count, <span class="hljs-function">(<span class="hljs-params">value</span>) =&gt;</span> {
      context.emit(<span class="hljs-string">'counter-update'</span>, value);
    });

    onMounted(<span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Composition API counter mounted'</span>));

    <span class="hljs-keyword">return</span> {
      count,
      increment,
      decrement,
      incrementBy,
      countPow,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Let’s analyze this code:</p>
<p>The entry point for all Composition API components is the new <code>setup</code> method. It is executed <strong>before</strong> the component is created and once the props are resolved. The function returns an object, and all of its properties are exposed to the rest of the component.</p>
<p>⚠️ We should avoid using <code>this</code> inside setup as it won’t refer to the component instance. <code>setup</code> is called before data properties, computed properties, or methods are resolved, so that they won’t be available within setup.</p>
<p>But we need to be careful: The variables we return from the setup method are, by default, not reactive.</p>
<p>We can use the <code>reactive</code> method to create a reactive state from a JavaScript object. Alternatively, we can use <code>ref</code> to make a standalone primitive value (for example, a string, number, or boolean) reactive:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { reactive, ref } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;

<span class="hljs-keyword">const</span> state = reactive({
  count: <span class="hljs-number">0</span>
})
<span class="hljs-built_in">console</span>.log(state.count); <span class="hljs-comment">// 0</span>

<span class="hljs-keyword">const</span> count = ref(<span class="hljs-number">0</span>);
<span class="hljs-built_in">console</span>.log(count.value); <span class="hljs-comment">// 0</span>
</code></pre>
<p>The <code>ref</code> object contains only one property named <code>value</code>, which can access the property value.</p>
<p>Vue 3 also provides different new methods like <code>computed</code>, <code>watch</code>, or <code>onMounted</code> that we can use in our <code>setup</code> method to implement the same logic we used in the Options API component.</p>
<h3 id="extract-composition-functionlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterextract-composition-function">Extract Composition Function<a class="post-section-overview" href="#extract-composition-function"></a></h3>
<p>But we can further improve our Vue component code by extracting the counter logic to a standalone <strong>composition function</strong> (<a target="_blank" href="https://github.com/Mokkapps/vue-3-composition-api-demo/blob/master/src/composables/useCounter.ts">useCounter</a>):</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { ref, computed, onMounted } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useCounter</span>(<span class="hljs-params">initialValue: <span class="hljs-built_in">number</span></span>) </span>{
  <span class="hljs-keyword">const</span> count = ref(initialValue);

  <span class="hljs-keyword">const</span> increment = <span class="hljs-function">() =&gt;</span> {
    count.value += <span class="hljs-number">1</span>;
  };
  <span class="hljs-keyword">const</span> decrement = <span class="hljs-function">() =&gt;</span> {
    count.value -= <span class="hljs-number">1</span>;
  };
  <span class="hljs-keyword">const</span> incrementBy = <span class="hljs-function">(<span class="hljs-params">value: <span class="hljs-built_in">number</span></span>) =&gt;</span> {
    count.value += value;
  };

  <span class="hljs-keyword">const</span> countPow = computed(<span class="hljs-function">() =&gt;</span> count.value * count.value);

  onMounted(<span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'useCounter mounted'</span>));

  <span class="hljs-keyword">return</span> {
    count,
    countPow,
    increment,
    decrement,
    incrementBy,
  };
}
</code></pre>
<p>This drastically reduces the code in our <a target="_blank" href="https://github.com/Mokkapps/vue-3-composition-api-demo/blob/master/src/components/CounterCompositionApiv2.vue">CounterCompositionApiv2.vue</a> component and additionally allows us to use the counter functionality in any other component:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { watch } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;
<span class="hljs-keyword">import</span> useCounter <span class="hljs-keyword">from</span> <span class="hljs-string">'../composables/useCounter'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">props</span>: {
    <span class="hljs-attr">initialValue</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">Number</span>,
      <span class="hljs-attr">default</span>: <span class="hljs-number">0</span>,
    },
  },
  <span class="hljs-attr">emits</span>: [<span class="hljs-string">'counter-update'</span>],
  setup(props, context) {
    <span class="hljs-keyword">const</span> { count, increment, countPow, decrement, incrementBy } = useCounter(
      props.initialValue
    );

    watch(count, <span class="hljs-function">(<span class="hljs-params">value</span>) =&gt;</span> {
      context.emit(<span class="hljs-string">'counter-update'</span>, value);
    });

    <span class="hljs-keyword">return</span> { count, countPow, increment, decrement, incrementBy };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>In Vue 2, <a target="_blank" href="https://vuejs.org/v2/guide/mixins.html#Basics">Mixins</a> were mainly used to share code between components. But they have a few issues:</p>
<ul>
<li>It’s impossible to pass parameters to the mixin to change its logic which drastically reduces its flexibility.</li>
<li>Property name conflicts can occur as properties from each mixin are merged into the same component.</li>
<li>It isn’t necessarily apparent which properties came from which mixin if a component uses multiple mixins.</li>
</ul>
<p>Composition API addresses all of these issues.</p>
<h3 id="sfc-script-setuplesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatersfc-script-setup">SFC Script Setup<a class="post-section-overview" href="#sfc-script-setup"></a></h3>
<p><a target="_blank" href="https://blog.vuejs.org/posts/vue-3.2.html">Vue 3.2</a> allows us to get rid of the <code>setup</code> method by providing the <code>&lt;script setup&gt;</code>. It’s the recommended syntax if you use Composition API and <a target="_blank" href="https://v3.vuejs.org/api/sfc-spec.html#sfc-syntax-specification">SFC (Single File Component)</a>.</p>
<p>This syntactic sugar provides several advantages over the normal <code>&lt;script&gt;</code> syntax:</p>
<ul>
<li>We can declare props and emitted events using TypeScript</li>
<li>Less boilerplate</li>
<li>More concise code</li>
<li>Better runtime performance: The template is compiled into a render function in the same scope, without an intermediate proxy</li>
<li>Better IDE type-inference performance: The language server has less work to extract types from code.</li>
</ul>
<p><a target="_blank" href="https://github.com/Mokkapps/vue-3-composition-api-demo/blob/master/src/components/CounterCompositionApiv3.vue">CounterCompositionApiv3.vue</a> demonstrates our counter example using the <code>&lt;script setup&gt;</code> syntax:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { defineProps, defineEmits, watch } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;
<span class="hljs-keyword">import</span> useCounter <span class="hljs-keyword">from</span> <span class="hljs-string">'../composables/useCounter'</span>;

interface Props {
  initialValue?: number;
}

<span class="hljs-keyword">const</span> props = withDefaults(defineProps&lt;Props&gt;(), {
  <span class="hljs-attr">initialValue</span>: <span class="hljs-number">0</span>,
});

<span class="hljs-keyword">const</span> emit = defineEmits([<span class="hljs-string">'counter-update'</span>]);

<span class="hljs-keyword">const</span> { count, countPow, increment, decrement, incrementBy } = useCounter(
  props.initialValue
);

watch(count, <span class="hljs-function">(<span class="hljs-params">value</span>) =&gt;</span> {
  emit(<span class="hljs-string">'counter-update'</span>, value);
});
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<h3 id="using-the-composition-api-with-vue-2lesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterusing-the-composition-api-with-vue-2">Using the Composition API with Vue 2<a class="post-section-overview" href="#using-the-composition-api-with-vue-2"></a></h3>
<p>If you can’t migrate to Vue 3 today, then you can still use the Composition API already. You can do this by installing <a target="_blank" href="https://github.com/vuejs/composition-api">the official Composition API Vue 2 Plugin</a>.</p>
<h2 id="conclusionlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>You’ve seen the same counter component created in Vue 2 using Options API and created in Vue 3 using Composition API.</p>
<p>Let’s summarize all the things I love about Composition API:</p>
<ul>
<li>More readable and maintainable code with the feature-wise separation of concerns brought with the composition API.</li>
<li>No more <code>this</code> keyword, so we can use arrow functions and use functional programming.</li>
<li>We can only access the things we return from the <code>setup</code> method, making things more readable.</li>
<li>Vue 3 is written in TypeScript and <a target="_blank" href="https://v3.vuejs.org/guide/typescript-support.html#using-with-composition-api">fully supports Composition API</a>.</li>
<li>Composition functions can easily be unit tested.</li>
</ul>
<p>The following image shows a large component where colors group its logical concerns and compares Options API versus Composition API:</p>
<p><a target="_blank" href="/static/97ade2c0a240b31abd5eb9205a399669/47311/vue-options-api-vs-composition-api.jpg"><img src="https://www.mokkapps.de/static/97ade2c0a240b31abd5eb9205a399669/15ec7/vue-options-api-vs-composition-api.jpg" alt="Vue Options API vs. Composition API" /></a>
<em>Vue Options API vs. Composition API</em></p>
<p>You can see that Composition API groups logical concerns, resulting in better maintainable code, especially for larger and complex components.</p>
<p>I can understand that many developers still prefer Options API as it is easier to teach people who are new to the framework and have JavaScript knowledge. But I would recommend that you use Composition API for complex applications that require a lot of domains and functionality. Additionally, Options API does not work very well with TypeScript, which is, in my opinion, also a must-have for complex applications.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[My Top Vue.js Interview Questions]]></title><description><![CDATA[This article summarizes a list of Vue.js interview questions that I would ask candidates and that I get often asked in interviews.
Table of Contents

1. What is Vue.js?
2. What are some of the main features of Vue.js?
3. Why would you choose Vue inst...]]></description><link>https://blog.mokkapps.de/my-top-vuejs-interview-questions</link><guid isPermaLink="true">https://blog.mokkapps.de/my-top-vuejs-interview-questions</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[vue]]></category><category><![CDATA[interview]]></category><category><![CDATA[Career]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Thu, 30 Sep 2021 07:48:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1632988149586/cTVM1RMCK.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This article summarizes a list of Vue.js interview questions that I would ask candidates and that I get often asked in interviews.</p>
<h2 id="table-of-contentslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatertable-of-contents">Table of Contents<a class="post-section-overview" href="#table-of-contents"></a></h2>
<ul>
<li><a class="post-section-overview" href="#1-what-is-vuejs">1. What is Vue.js?</a></li>
<li><a class="post-section-overview" href="#2-what-are-some-of-the-main-features-of-vuejs">2. What are some of the main features of Vue.js?</a></li>
<li><a class="post-section-overview" href="#3-why-would-you-choose-vue-instead-of-react-or-angular">3. Why would you choose Vue instead of React or Angular?</a></li>
<li><a class="post-section-overview" href="#4-what-is-an-sfc">4. What is an SFC?</a></li>
<li><a class="post-section-overview" href="#5-what-are-computed-properties">5. What are computed properties?</a></li>
<li><a class="post-section-overview" href="#6-what-are-watchers">6. What are watchers?</a></li>
<li><a class="post-section-overview" href="#7-what-is-the-difference-between-registering-a-component-locally-and-globally">7. What is the difference between registering a component locally and globally?</a></li>
<li><a class="post-section-overview" href="#8-what-are-some-of-the-most-important-directives-in-vue">8. What are some of the most important directives in Vue?</a></li>
<li><a class="post-section-overview" href="#9-what-is-the-vue-application-instance">9. What is the Vue application instance?</a></li>
<li><a class="post-section-overview" href="#10-what-is-the-difference-between-one-way-data-flow-and-two-way-data-binding">10. What is the difference between one-way data flow and two-way data binding?</a></li>
<li><a class="post-section-overview" href="#11-what-is-the-difference-between-a-slot-and-a-scoped-slot">11. What is the difference between a slot and a scoped slot?</a></li>
<li><a class="post-section-overview" href="#12-how-does-vue-know-when-it-should-rerender">12. How does Vue know when it should rerender?</a></li>
<li><a class="post-section-overview" href="#13-how-can-code-be-reused-between-components">13. How can code be reused between components?</a></li>
<li><a class="post-section-overview" href="#14-how-to-optimize-vuejs-performance">14. How to optimize Vue.js performance?</a></li>
<li><a class="post-section-overview" href="#15-which-lifecycles-and-corresponding-hooks-are-available-in-vue">15. Which lifecycles and corresponding hooks are available in Vue?</a></li>
<li><a class="post-section-overview" href="#conclusion">Conclusion</a></li>
</ul>
<h2 id="1-what-is-vuejslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater1-what-is-vuejs">1. What is Vue.js?<a class="post-section-overview" href="#1-what-is-vuejs"></a></h2>
<p><a target="_blank" href="https://vuejs.org">Vue</a> is a progressive framework for building user interfaces that was designed to be incrementally adoptable. Its core library is focused exclusively on the view layer so that it can easily be integrated with other projects or libraries.</p>
<p>But in contrast to <a target="_blank" href="https://reactjs.org/">React</a>, Vue provides companion libraries for routing and state management which are all officially supported and kept up-to-date with the core library.</p>
<h2 id="2-what-are-some-of-the-main-features-of-vuejslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater2-what-are-some-of-the-main-features-of-vuejs">2. What are some of the main features of Vue.js?<a class="post-section-overview" href="#2-what-are-some-of-the-main-features-of-vuejs"></a></h2>
<ul>
<li>Virtual DOM: Vue uses a <a target="_blank" href="https://vuejs.org/v2/guide/render-function.html#The-Virtual-DOM">Virtual DOM</a>, similar to other frameworks such as React, Ember, etc.</li>
<li>Components: Components are the basic building block for reusable elements in Vue applications.</li>
<li>Templates: Vue uses HTML-based templates.</li>
<li>Routing: Vue provide it’s <a target="_blank" href="https://router.vuejs.org/">own router</a>.</li>
<li>Built-in <a target="_blank" href="https://v3.vuejs.org/api/directives.html">directives</a>: For example, v-if or v-for</li>
<li>Lightweight: Vue is a lightweight library compared to other frameworks.</li>
</ul>
<h2 id="3-why-would-you-choose-vue-instead-of-react-or-angularlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater3-why-would-you-choose-vue-instead-of-react-or-angular">3. Why would you choose Vue instead of React or Angular?<a class="post-section-overview" href="#3-why-would-you-choose-vue-instead-of-react-or-angular"></a></h2>
<p>Vue.js combines the best parts of Angular and React. Vue.js is a more flexible, less opinionated solution than Angular but it’s still a framework and not a UI library like React</p>
<p>I recently decided to focus my freelancer career on <a target="_blank" href="https://vuejs.org">Vue.js</a>, you can read more about this decision in the <a target="_blank" href="https://www.mokkapps.de/blog/why-i-picked-vue-js-as-my-freelancer-niche/">corresponding blog post</a>.</p>
<h2 id="4-what-is-an-sfclesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater4-what-is-an-sfc">4. What is an SFC?<a class="post-section-overview" href="#4-what-is-an-sfc"></a></h2>
<p>Vue <a target="_blank" href="https://v3.vuejs.org/guide/single-file-component.html">Single File Components</a> (aka <code>*.vue</code> files, abbreviated as SFC) is a special file format that allows us to encapsulate the template (<code>&lt;template&gt;</code>), logic (<code>&lt;script&gt;</code>), and styling (<code>&lt;style&gt;</code>) of a Vue component in a single file.</p>
<p>Vue SFC is a framework-specific file format and must be pre-compiled by <a target="_blank" href="https://github.com/vuejs/vue-next/tree/master/packages/compiler-sfc">@vue/compiler-sfc</a> into standard JavaScript and CSS. A compiled SFC is a standard JavaScript (ES) module.</p>
<h2 id="5-what-are-computed-propertieslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater5-what-are-computed-properties">5. What are computed properties?<a class="post-section-overview" href="#5-what-are-computed-properties"></a></h2>
<p><a target="_blank" href="https://v3.vuejs.org/guide/computed.html#computed-properties">Computed properties</a> should be used to remove as much logic as possible from the templates as otherwise the template gets bloated and is harder to maintain. If you have complex logic including reactive data in your template you should use a computed property instead.</p>
<p>Instead of methods, computed properties are cached based on their reactive dependencies. A computed property will only re-evaluate when some of its reactive dependencies have changed.</p>
<h2 id="6-what-are-watcherslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater6-what-are-watchers">6. What are watchers?<a class="post-section-overview" href="#6-what-are-watchers"></a></h2>
<p><a target="_blank" href="https://v3.vuejs.org/guide/computed.html#watchers">Watchers</a> should be used when asynchronous or expensive operations need to be executed in response to changing data.</p>
<h2 id="7-what-is-the-difference-between-registering-a-component-locally-and-globallylesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater7-what-is-the-difference-between-registering-a-component-locally-and-globally">7. What is the difference between registering a component locally and globally?<a class="post-section-overview" href="#7-what-is-the-difference-between-registering-a-component-locally-and-globally"></a></h2>
<p>If a component is <a target="_blank" href="https://v3.vuejs.org/guide/component-registration.html#global-registration">registered globally</a> it can be used in the template of any component instance within this application:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> app = Vue.createApp({});

app.component(<span class="hljs-string">'component-a'</span>, {
  <span class="hljs-comment">/* ... */</span>
});

app.mount(<span class="hljs-string">'#app'</span>);
</code></pre>
<p>Global registration can unnecessarily increase your JavaScript bundle if you are using build systems like Webpack. If you stop using a component in your code, it will still be included in the final build.</p>
<p>To avoid this, we can <a target="_blank" href="https://v3.vuejs.org/guide/component-registration.html#local-registration">register components locally</a> by defining them in the component where it is needed:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> ComponentA <span class="hljs-keyword">from</span> <span class="hljs-string">'./ComponentA.vue'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">components</span>: {
    ComponentA,
  },
  <span class="hljs-comment">// ...</span>
};
</code></pre>
<p>Note that locally registered components are not available in subcomponents.</p>
<h2 id="8-what-are-some-of-the-most-important-directives-in-vuelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater8-what-are-some-of-the-most-important-directives-in-vue">8. What are some of the most important directives in Vue?<a class="post-section-overview" href="#8-what-are-some-of-the-most-important-directives-in-vue"></a></h2>
<ul>
<li><a target="_blank" href="https://v3.vuejs.org/api/directives.html#v-if">v-if</a> adds or removes DOM elements based on the given expression.</li>
<li><a target="_blank" href="https://v3.vuejs.org/api/directives.html#v-else">v-else</a> displays content only when the expression adjacent v-if resolves to false.</li>
<li><a target="_blank" href="https://v3.vuejs.org/api/directives.html#v-show">v-show</a> is similar to v-if, but it renders all elements to the DOM and then uses the CSS display property to show/hide elements.</li>
<li><a target="_blank" href="https://v3.vuejs.org/api/directives.html#v-for">v-for</a> allows us to loop through items in an array or object.</li>
<li><a target="_blank" href="https://v3.vuejs.org/api/directives.html#v-model">v-model</a> is used for two-way data bindings.</li>
<li><a target="_blank" href="https://v3.vuejs.org/api/directives.html#v-on">v-on</a> attaches an event listener to the element.</li>
</ul>
<p><a target="_blank" href="https://v3.vuejs.org/api/directives.html#v-model">Here</a> you can find all available directives.</p>
<h2 id="9-what-is-the-vue-application-instancelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater9-what-is-the-vue-application-instance">9. What is the Vue application instance?<a class="post-section-overview" href="#9-what-is-the-vue-application-instance"></a></h2>
<p>The <a target="_blank" href="https://v3.vuejs.org/guide/instance.html#creating-an-application-instance">application instance</a> is used to register <em>globals</em> that can then be used by components within that application. An application instance is created with the <code>createApp</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> app = Vue.createApp({
  <span class="hljs-comment">/* options */</span>
});
</code></pre>
<p>In Vue 2 this was called <a target="_blank" href="https://vuejs.org/v2/guide/instance.html">Vue instance</a> and created this way:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> vm = <span class="hljs-keyword">new</span> Vue({
  <span class="hljs-comment">// options</span>
});
</code></pre>
<h2 id="10-what-is-the-difference-between-one-way-data-flow-and-two-way-data-bindinglesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater10-what-is-the-difference-between-one-way-data-flow-and-two-way-data-binding">10. What is the difference between one-way data flow and two-way data binding?<a class="post-section-overview" href="#10-what-is-the-difference-between-one-way-data-flow-and-two-way-data-binding"></a></h2>
<p><a target="_blank" href="/static/1092c69ce0c1b6d0194e787a58dc09ab/d9c39/data-binding.jpg"><img src="https://www.mokkapps.de/static/1092c69ce0c1b6d0194e787a58dc09ab/15ec7/data-binding.jpg" alt="One-Way &amp; Two-Way Data Binding" /></a></p>One-Way &amp; Two-Way Data Binding<p></p>
<p>Vue uses a one-way data flow. Parents can pass data to child components using a prop and bind the data using the <a target="_blank" href="https://vuejs.org/v2/api/#v-bind">v-bind directive</a>. When the parent component updates the prop value, it’s automatically updated in the child component. You should avoid mutating the property inside the child component and it will not affect the property in the parent component (unless it’s an array or object). Using <a target="_blank" href="https://vuejs.org/v2/api/#v-bind">Events</a> the child component can communicate back to the parent.</p>
<p>Vue provides the <a target="_blank" href="https://vuejs.org/v2/api/#v-model">v-model directive</a> for two-way data binding of form inputs. <code>v-model</code> is just syntax sugar for <code>v-bind</code> combined with <code>v-on:input</code>.</p>
<h2 id="11-what-is-the-difference-between-a-slot-and-a-scoped-slotlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater11-what-is-the-difference-between-a-slot-and-a-scoped-slot">11. What is the difference between a slot and a scoped slot?<a class="post-section-overview" href="#11-what-is-the-difference-between-a-slot-and-a-scoped-slot"></a></h2>
<p>A <a target="_blank" href="https://v3.vuejs.org/guide/component-slots.html#slots">slot</a> is a placeholder in a child component that is filled with content passed from the parent. Content of a regular slot is compiled in the parent’s scope and then passed to the child component.</p>
<p>Scoped slots are needed if the slot content needs to have access to data only available in the child component. We can bind attributes a <code>&lt;slot&gt;</code>, these elements are called <strong>slot props</strong>. Now, in the parent scope, we can use <code>v-slot</code> with a value to define a name for the slot props we’ve been provided:</p>
<p><a target="_blank" href="/static/4a5efa364ae5e8b1e637c66a2a149f1c/3e6fe/scoped-slot.jpg"><img src="https://www.mokkapps.de/static/4a5efa364ae5e8b1e637c66a2a149f1c/15ec7/scoped-slot.jpg" alt="Vue Scoped Slot" /></a></p>Vue Scoped Slot<p></p>
<h2 id="12-how-does-vue-know-when-it-should-rerenderlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater12-how-does-vue-know-when-it-should-rerender">12. How does Vue know when it should rerender?<a class="post-section-overview" href="#12-how-does-vue-know-when-it-should-rerender"></a></h2>
<p>The following explanation is based on <a target="_blank" href="https://v3.vuejs.org/guide/reactivity-fundamentals.html#reactivity-fundamentals">Vue 3’s Reactivity</a>. <a target="_blank" href="https://www.sitepoint.com/vue-3-reactivity-system/">This article</a> describes how Vue 2 implemented reactivity.</p>
<p>Vue uses an unobtrusive reactivity system which is one its most distinct features.</p>
<p>But what is reactivity? Reactivity is a programming paradigm that allows us to adjust to changes in a declarative manner.</p>
<p>The official documentation uses an Excel spreadsheet to demonstrate reactivity:</p>
<p><a target="_blank" href="https://v3.vuejs.org/images/reactivity-spreadsheet.mp4"><img src="https://www.mokkapps.de/static/ca7df137f80eb95ed239e6ca1d5150a1/15ec7/reactivity-video-thumbnail.jpg" alt="Vue Reactivity Spreadsheet Video" />Vue Reactivity Spreadsheet Video</a></p>
<p>As you can see, we get the SUM if we put the number 2 in the first cell and number 3 in the second cell. The reactive part happens if we update the first number and the SUM automatically gets updated too.</p>
<p>Unfortunately, JavaScript variables are not reactive by default:</p>
<pre><code class="lang-js"><span class="hljs-keyword">let</span> value1 = <span class="hljs-number">1</span>;
<span class="hljs-keyword">let</span> value2 = <span class="hljs-number">2</span>;
<span class="hljs-keyword">let</span> sum = value1 + value2;

<span class="hljs-built_in">console</span>.log(sum); <span class="hljs-comment">// 5</span>
value1 = <span class="hljs-number">3</span>;
<span class="hljs-built_in">console</span>.log(sum); <span class="hljs-comment">// Still 5</span>
</code></pre>
<p>Therefore, Vue 3 added an abstraction on <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy">JavaScript Proxy</a> to be able to achieve the reactiveness.</p>
<p>In Vue 3, we can easily declare a reactive state using the <code>reactive</code> method</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { reactive } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;

<span class="hljs-comment">// reactive state</span>
<span class="hljs-keyword">const</span> state = reactive({
  <span class="hljs-attr">count</span>: <span class="hljs-number">0</span>,
});
</code></pre>
<p>or create standalone reactive values as <code>refs</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;

<span class="hljs-keyword">const</span> count = ref(<span class="hljs-number">0</span>);
<span class="hljs-built_in">console</span>.log(count.value); <span class="hljs-comment">// 0</span>

count.value++;
<span class="hljs-built_in">console</span>.log(count.value); <span class="hljs-comment">// 1</span>
</code></pre>
<h2 id="13-how-can-code-be-reused-between-componentslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater13-how-can-code-be-reused-between-components">13. How can code be reused between components?<a class="post-section-overview" href="#13-how-can-code-be-reused-between-components"></a></h2>
<p>In Vue 2 and 3 we can use <a target="_blank" href="https://vuejs.org/v2/guide/mixins.html">Mixins</a> to reuse code between components.</p>
<p>Since Vue 3 the framework provides the <a target="_blank" href="https://v3.vuejs.org/guide/composition-api-introduction.html#why-composition-api">Composition API</a> that resolves the <a target="_blank" href="https://v3.vuejs.org/guide/composition-api-introduction.html#why-composition-api">Mixins drawbacks</a>.</p>
<h2 id="14-how-to-optimize-vuejs-performancelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater14-how-to-optimize-vuejs-performance">14. How to optimize Vue.js performance?<a class="post-section-overview" href="#14-how-to-optimize-vuejs-performance"></a></h2>
<p>Use <a target="_blank" href="https://v3.vuejs.org/guide/ssr/routing.html#code-splitting">code splitting</a> (also known as lazy loading) to reduce the size of assets that need to be downloaded by the browser for the initial render. Essentially, it helps to load just the parts of the initial screen that are currently needed. All other parts of the application are downloaded when they are needed and requested:</p>
<pre><code class="lang-js"><span class="hljs-comment">// the MyUser component is dynamically loaded if the `/user route is visited:</span>
<span class="hljs-keyword">const</span> routes = [
  { <span class="hljs-attr">path</span>: <span class="hljs-string">'/user'</span>, <span class="hljs-attr">component</span>: <span class="hljs-function">() =&gt;</span> <span class="hljs-keyword">import</span>(<span class="hljs-string">'./components/MyUser.vue'</span>) },
];
</code></pre>
<h2 id="15-which-lifecycles-and-corresponding-hooks-are-available-in-vuelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater15-which-lifecycles-and-corresponding-hooks-are-available-in-vue">15. Which lifecycles and corresponding hooks are available in Vue?<a class="post-section-overview" href="#15-which-lifecycles-and-corresponding-hooks-are-available-in-vue"></a></h2>
<p>Each Vue component instance goes through a series of initialization steps when it’s created. For example, it needs to set up data observation, compile the template, mount the instance to the DOM, and update the DOM when data changes. Along the way, it also runs functions called <a target="_blank" href="https://v3.vuejs.org/guide/instance.html#lifecycle-hooks">lifecycle hooks</a>, which allows us to execute custom code at specific stages.</p>
<ul>
<li>Creation hooks (<code>beforeCreate</code> and <code>created</code>) allow us to perform actions before the component has even been added to the DOM. These hooks are also executed during server-side rendering. The <code>created</code> hook is the perfect lifecycle hook to trigger HTTP requests and populate any initial data that the component needs.</li>
<li>Mounting hooks (<code>beforeMount</code>, <code>mounted</code>) are often the most-used hooks and allow us to access the component immediately before and after the first render. The <code>mounted</code> hook is an ideal time to integrate 3rd party libraries or to access the DOM.</li>
<li>Updating hooks (<code>beforeUpdate</code>, <code>updated</code>) are called whenever a reactive property used by the component changes, or something else causes it to re-render. In the <code>updated</code> hook the DOM and the model are in-sync while in the <code>beforeUpdate</code> hook only the model is updated but not the DOM.</li>
<li>Destruction hooks (<code>beforeDestroy</code>, <code>destroyed</code>) allow us to perform actions when the component is destroyed, such as cleanup or sending analytics. In the <code>beforeDestroy</code> hook we still have access to the Vue instance and can, for example, emit events. <code>destroyed</code> is the ideal place for a final cleanup, e.g. removing event listeners. </li>
</ul>
<p>Below is a diagram for the instance lifecycle:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1632988147066/cQ0a7XQVm.svg+xml" alt="Vue Instance Lifecycle Diagram" /></p>
<p>There exists an additional interesting lifecycle hook called <a target="_blank" href="https://v3.vuejs.org/api/options-lifecycle-hooks.html#unmounted">errorCaptured</a> which is called when an error from any descendent component is captured.</p>
<p>This hook receives three arguments: the error, the component instance that triggered the error, and a string containing information on where the error was captured. The hook can return false to stop the error from propagating further.</p>
<h2 id="conclusionlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>With these interview questions, you should be well-prepared for any upcoming job interviews. Let me know in the comments if you would ask any other important questions about Vue.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>Alternatively (or additionally), you can also <a target="_blank" href="https://mokkapps.de/newsletter">subscribe to my newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Why I Picked Vue.js as My Freelancer Niche]]></title><description><![CDATA[I have professional experience with the three big players in web development: Angular, Vue.js and React. I’ve reached the point in my career where I need to choose one of the three frameworks/libraries that I will use for my future freelancing career...]]></description><link>https://blog.mokkapps.de/why-i-picked-vuejs-as-my-freelancer-niche</link><guid isPermaLink="true">https://blog.mokkapps.de/why-i-picked-vuejs-as-my-freelancer-niche</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[Freelancing]]></category><category><![CDATA[freelance]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[Career]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Tue, 21 Sep 2021 08:50:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1632214272907/TrQ-jxkTV.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have professional experience with the three big players in web development: <a target="_blank" href="https://angular.io">Angular</a>, <a target="_blank" href="https://vuejs.org/">Vue.js</a> and <a target="_blank" href="https://reactjs.org/">React</a>. I’ve reached the point in my career where I need to choose one of the three frameworks/libraries that I will use for my future freelancing career.</p>
<p>As the title already reveals, I chose Vue and in this article, I will describe to you why I picked it instead of React or Angular.</p>
<p>⚠️ This article will not provide a full comparison between the three technologies.</p>
<h2 id="table-of-contentslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatertable-of-contents">Table of Contents<a class="post-section-overview" href="#table-of-contents"></a></h2>
<ul>
<li><a class="post-section-overview" href="#why-do-i-need-a-niche">Why Do I Need a Niche?</a></li>
<li><a class="post-section-overview" href="#my-freelancing-history">My Freelancing History</a></li>
<li><a class="post-section-overview" href="#what-i-love-about-vue">What I Love About Vue</a></li>
<li><a class="post-section-overview" href="#what-i-dont-like-in-vue">What I Don’t Like In Vue</a></li>
<li><a class="post-section-overview" href="#what-i-miss-in-vue">What I Miss In Vue</a></li>
<li><a class="post-section-overview" href="#vues-popularity">Vue’s Popularity</a></li>
<li><a class="post-section-overview" href="#conclusion">Conclusion</a></li>
</ul>
<h2 id="why-do-i-need-a-nichelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterwhy-do-i-need-a-niche">Why Do I Need a Niche?<a class="post-section-overview" href="#why-do-i-need-a-niche"></a></h2>
<p><a target="_blank" href="/static/e5a7913be44c6ec5f386bf8c73908721/eea4a/whiteboard-audience.jpg"><img src="https://www.mokkapps.de/static/e5a7913be44c6ec5f386bf8c73908721/15ec7/whiteboard-audience.jpg" alt="Find Your Audience" /></a></p>Find Your Audience<p></p>
<p>Finding your niche as a freelancer can have an extremely positive impact on your career. It took me some time to find mine, but finally, I found it and I can take my business to the next level. It has some advantages to be a jack of all but in the end, it’s even better to be the master of one trade. Having a niche can boost your income, helps to find new projects easier, and is useful to advertise yourself as an expert.</p>
<p>I can also give you an example of how the niche saves me time every day:</p>
<p>My previous search queries for job agents on freelancer platforms looked like this: <code>React OR Angular OR TypeScript OR JavaScript OR React Native OR Vue</code>. This way, I got job agent emails with dozens of job offers that I had to manually scan for interesting projects.</p>
<p>With a niche in place, I modified these search queries to <code>Vue</code> and now the job agent emails contain only a few freelancer projects but they are tailored to my skills.</p>
<h2 id="my-freelancing-historylesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatermy-freelancing-history">My Freelancing History<a class="post-section-overview" href="#my-freelancing-history"></a></h2>
<p>When I started freelancing back in 2019 my tech focus was on web development using the <a target="_blank" href="https://angular.io">Angular</a> framework. But for my first freelancing project I choose a <a target="_blank" href="https://vuejs.org/">Vue.js</a> project and I stayed there for about two years. I chose this project because I already had professional experience with Angular and some experience with React as I used it for my <a target="_blank" href="https://mokkapps.de">portfolio website</a> and two React Native apps that I developed and published. I wanted to see how it compares to Angular and React. After this project, beginning from January to September 2021 I worked in a <a target="_blank" href="https://reactjs.org/">React</a> project as I wanted to gain some professional experience with the library.</p>
<p>I could easily further specialize in Angular, but I have no good belly feeling with this choice. Therefore, I had to choose between React and Vue.</p>
<h2 id="what-i-love-about-vuelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterwhat-i-love-about-vue">What I Love About Vue<a class="post-section-overview" href="#what-i-love-about-vue"></a></h2>
<blockquote>
<p>TL;DR: In my opinion, Vue.js combines the best parts of Angular and React. Vue.js is a more flexible, less opinionated solution than Angular but it’s still a framework and not a UI library like React.</p>
</blockquote>
<p><a target="_blank" href="/static/b81b38b8a530103ace13f198f6d5f6b0/d9c39/framework-comparison.jpg"><img src="https://www.mokkapps.de/static/b81b38b8a530103ace13f198f6d5f6b0/15ec7/framework-comparison.jpg" alt="Framework Comparison" /></a></p>Framework Comparison<p></p>
<h3 id="less-usage-of-javascripts-this-keywordlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterless-usage-of-javascripts-this-keyword">Less Usage of JavaScript’s “this” keyword<a class="post-section-overview" href="#less-usage-of-javascripts-this-keyword"></a></h3>
<p>Angular components are full of the JavaScript keyword <code>this</code>. I don’t like this and thankfully we can write React and Vue components without the <code>this</code> keyword by using <a target="_blank" href="https://reactjs.org/docs/hooks-intro.html">React Hooks</a> and <a target="_blank" href="https://v3.vuejs.org/api/composition-api.html">Vue 3’s Composition API</a>.</p>
<h3 id="outstanding-documentationlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreateroutstanding-documentation">Outstanding Documentation<a class="post-section-overview" href="#outstanding-documentation"></a></h3>
<p>The <a target="_blank" href="https://v3.vuejs.org/guide/introduction.html">official Vue documentation</a> is amazing and one of the best resources to learn Vue.</p>
<h3 id="best-parts-of-react-and-angularlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterbest-parts-of-react-and-angular">Best Parts of React and Angular<a class="post-section-overview" href="#best-parts-of-react-and-angular"></a></h3>
<p>In its early development phase, Vue took inspiration from the good things of <a target="_blank" href="https://angularjs.org/">AngularJS</a> (the first version of Angular). Vue also got inspired by React and they share some similarities:</p>
<ul>
<li>They have their focus in the core library. Concerns like global state management and routing are handled by separate companion libraries.</li>
<li>Both provide reactive and composable view components.</li>
<li>One and the other use a virtual DOM.</li>
</ul>
<h3 id="less-optimization-effortslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterless-optimization-efforts">Less Optimization Efforts<a class="post-section-overview" href="#less-optimization-efforts"></a></h3>
<p>In Vue, I need to care less about optimization efforts in comparison to React. React triggers a re-rendering of the entire component tree when a component’s state changes. Read my article <a target="_blank" href="https://www.mokkapps.de/blog/debug-why-react-re-renders-a-component/">“Debug Why React (Re-)Renders a Component”</a> for further details.</p>
<p>There are multiple ways to avoid unnecessary re-rendering of child components in React:</p>
<ul>
<li>use <a target="_blank" href="https://reactjs.org/docs/react-api.html#reactpurecomponent">PureComponent</a></li>
<li>implement <code>shouldComponentUpdate</code> if you are using class components</li>
<li>use immutable data structures</li>
</ul>
<p>Angular developers also need to take care of the change detection, you can read my article <a target="_blank" href="https://www.mokkapps.de/blog/the-last-guide-for-angular-change-detection-you-will-ever-need/">“The Last Guide For Angular Change Detection You’ll Ever Need”</a> if you want to deep-dive into that mechanism.</p>
<p>Vue automatically tracks a component’s dependencies during its render. Therefore, it knows precisely which components need to be re-rendered when the state changes. As a Vue developer, I can more focus on building the app than on performance optimizations.</p>
<h3 id="templateslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatertemplates">Templates<a class="post-section-overview" href="#templates"></a></h3>
<p>Vue uses HTML templates, but there’s an option to write the render function in <a target="_blank" href="https://reactjs.org/docs/introducing-jsx.html">JSX</a>. On the other hand, in React there’s solely JSX. A Vue component is split into three parts: HTML (<code>&lt;template&gt;</code>), CSS (<code>&lt;style&gt;</code>) and JavaScript (<code>&lt;script&gt;</code>) which most web developers should already be familiar with.</p>
<p>In React, we use JSX to render our component’s template. This has some advantages:</p>
<ul>
<li>You don’t have to learn an extra DSL (Domain-Specific Language) but can use pure JavaScript to build your view.</li>
<li>JSX has good tooling support (e.g. linting, type checking, and editor auto-completion).</li>
</ul>
<p>I liked this approach in the beginning but I have changed my opinion. For beginners, it might be really helpful to write the templates in JSX because they need to learn JavaScript basics like <code>Array.prototype.map()</code> to render lists. But writing code using an extra DSL (Domain-Specific Language) helps us to write less code.</p>
<p><a target="_blank" href="https://twitter.com/Rich_Harris">Rich Harris</a> wrote a great blog post <a target="_blank" href="https://svelte.dev/blog/write-less-code">“Write less code”</a> that reminds us that all code is buggy:</p>
<blockquote>
<p>All code is buggy. It stands to reason, therefore, that the more code you have to write the buggier your apps will be.</p>
</blockquote>
<p>More code</p>
<ul>
<li>takes more time</li>
<li>number of bugs grows quadratically with the size of the codebase</li>
<li>is harder to review in the code review process</li>
</ul>
<p>So using the DSL provided by Angular and Vue we write less code which is good.</p>
<h3 id="scaling-uplesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterscaling-up">Scaling Up<a class="post-section-overview" href="#scaling-up"></a></h3>
<p>React provides a robust routing (<a target="_blank" href="https://reactrouter.com/">React Router</a>) and state management solution (<a target="_blank" href="https://redux.js.org/">Redux</a>) that are maintained by the community and create a more fragmented ecosystem. Vue provides companion libraries for routing and state management which are all officially supported and kept up-to-date with the core library. This is similar to the Angular framework which provides a collection of well-integrated libraries that cover a wide variety of features, including routing, forms management, client-server communication, and more.</p>
<h3 id="scaling-downlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterscaling-down">Scaling Down<a class="post-section-overview" href="#scaling-down"></a></h3>
<p>Getting started with Vue is much simpler than with React or Angular as you can use it completely without any build system and just use a single script tag in your HTML.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdn.jsdelivr.net/npm/vue@3"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>I already used this simple approach in a large <a target="_blank" href="http://www.djangoproject.com/">Django</a> monolithic application to be able to add new UI features without using the Django templating engine based on Python.</p>
<p>I know it’s also possible to use React with Babel Standalone but this is not a suitable approach for production usage.</p>
<h3 id="typescriptlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatertypescript">TypeScript<a class="post-section-overview" href="#typescript"></a></h3>
<p>Vue 3 is written in <a target="_blank" href="https://www.typescriptlang.org/">TypeScript</a> so we don’t need any additional tooling to use TypeScript with Vue - it has first-class citizen support. Angular is even stricter and uses TypeScript per default.</p>
<h2 id="what-i-dont-like-in-vuelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterwhat-i-dont-like-in-vue">What I Don’t Like In Vue<a class="post-section-overview" href="#what-i-dont-like-in-vue"></a></h2>
<h3 id="communitylesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatercommunity">Community<a class="post-section-overview" href="#community"></a></h3>
<p>Vue already has a good community but React is way more popular and therefore the community is bigger.</p>
<h3 id="less-opinionatedlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterless-opinionated">Less opinionated<a class="post-section-overview" href="#less-opinionated"></a></h3>
<p>Angular is very opinionated and I like that for large applications. Vue is less opinionated and has no restrictions on how you structure your code or which build system you use. I like to have some restrictions in my framework as this reduces discussion time between developers.</p>
<h2 id="what-i-miss-in-vuelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterwhat-i-miss-in-vue">What I Miss In Vue<a class="post-section-overview" href="#what-i-miss-in-vue"></a></h2>
<h3 id="better-mobile-app-supportlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterbetter-mobile-app-support">Better Mobile App Support<a class="post-section-overview" href="#better-mobile-app-support"></a></h3>
<p>Vue.js does not natively support mobile app development. There are a <a target="_blank" href="https://v3.vuejs.org/guide/mobile.html#hybrid-app-development">number of solutions</a> for creating native iOS and Android apps with Vue.js but React and <a target="_blank" href="https://reactnative.dev/">React Native</a> provide a better experience for React developers.</p>
<h3 id="angulars-http-modulelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterangulars-http-module">Angular’s HTTP module<a class="post-section-overview" href="#angulars-http-module"></a></h3>
<p>I love that Angular provides a separate <a target="_blank" href="https://angular.io/guide/http">HTTP module</a> to be able to communicate with servers using the HTTP protocol. It provides features like:</p>
<ul>
<li>The ability to request typed response objects.</li>
<li>Streamlined error handling.</li>
<li>Testability features.</li>
<li>Request and response interception.</li>
</ul>
<p>Vue provides no specific library to make HTTP requests so you can either use the Fetch API or libraries like <a target="_blank" href="https://github.com/axios/axios">axios</a>.</p>
<h3 id="angulars-forms-modulelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterangulars-forms-module">Angular’s Forms Module<a class="post-section-overview" href="#angulars-forms-module"></a></h3>
<p>Vue does not provide a form validation pattern like Angular’s <a target="_blank" href="https://angular.io/guide/reactive-forms">Reactive Forms</a> but we can use 3rd party libraries like <a target="_blank" href="https://vuelidate-next.netlify.app">Vuelidate</a>.</p>
<h2 id="vues-popularitylesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatervues-popularity">Vue’s Popularity<a class="post-section-overview" href="#vues-popularity"></a></h2>
<p>In <a target="_blank" href="https://insights.stackoverflow.com/survey/2020#overview">StackOverflow’s 2020 Developer Survey</a> we can see that Vue.js is not as popular as Angular and React:</p>
<p><a target="_blank" href="/static/dda4c5ff03c7e22ad702c73017e17f53/d9c39/stackoverflow-web-frameworks.jpg"><img src="https://www.mokkapps.de/static/dda4c5ff03c7e22ad702c73017e17f53/15ec7/stackoverflow-web-frameworks.jpg" alt="StackOverflow 2020 Developer Survey" /></a></p>StackOverflow 2020 Developer Survey<p></p>
<p>We can see the same in <a target="_blank" href="https://research.hackerrank.com/developer-skills/2020">HackerRank’s 2020 Developer Skills Report</a>. But in this report Vue.js has shown steady growth, rising one spot per year since 2018:</p>
<p><a target="_blank" href="/static/1f9babcfc2bc937410c8658bc748f5a6/c23a4/hacker-rank-best-known-frameworks-2018-2020.jpg"><img src="https://www.mokkapps.de/static/1f9babcfc2bc937410c8658bc748f5a6/15ec7/hacker-rank-best-known-frameworks-2018-2020.jpg" alt="HackerRank 2020 Developer Skills Report: Best known frameworks 2018-2020" /></a></p>HackerRank 2020 Developer Skills Report: Best known frameworks 2018-2020<p></p>
<p>Additionally, 23.6% of the developers want to learn Vue.js next:</p>
<p><a target="_blank" href="/static/359b135ac7a8c87da4297c82c35c7230/7e7db/hacker-rank-learn-next.jpg"><img src="https://www.mokkapps.de/static/359b135ac7a8c87da4297c82c35c7230/15ec7/hacker-rank-learn-next.jpg" alt="HackerRank 2020 Developer Skills Report: Which frameworks do you plan on learning next?" /></a></p>HackerRank 2020 Developer Skills Report: Which frameworks do you plan on learning next?<p></p>
<p><a target="_blank" href="https://2020.stateofjs.com/en-US/">The State of JavaScript Survey 2020</a> shows that developers are more and more interested in React and Vue.js but get less interested in Angular:</p>
<p><a target="_blank" href="/static/51b81bfab8e0c2400048fb1ecb2f7356/d9c39/state-of-js-2020-positive-negative-split.jpg"><img src="https://www.mokkapps.de/static/51b81bfab8e0c2400048fb1ecb2f7356/15ec7/state-of-js-2020-positive-negative-split.jpg" alt="State of JavaScript 2020: Positive/Negative Split" /></a></p>State of JavaScript 2020: Positive/Negative Split<p></p>
<h2 id="conclusionlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>I’m pretty happy with my choice and the future will show if it was the right decision or not.</p>
<p>Which framework or technology did you pick for your niche? Tell me in the comments!</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
]]></content:encoded></item><item><title><![CDATA[Track Twitter Follower Growth Over Time Using A Serverless Node.js API on AWS Amplify]]></title><description><![CDATA[In March 2021 I started to use FeedHive to help me grow an audience on Twitter. Recently, I wanted to check how my Twitter followers have grown over time. Unfortunately, Twitter Analytics only provides data from the last 30 days. So I decided to deve...]]></description><link>https://blog.mokkapps.de/track-twitter-follower-growth-over-time-using-a-serverless-nodejs-api-on-aws-amplify</link><guid isPermaLink="true">https://blog.mokkapps.de/track-twitter-follower-growth-over-time-using-a-serverless-nodejs-api-on-aws-amplify</guid><category><![CDATA[Node.js]]></category><category><![CDATA[Twitter]]></category><category><![CDATA[serverless]]></category><category><![CDATA[AWS]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Mon, 13 Sep 2021 08:44:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1631522716306/jOAH0DiNU0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In March 2021 I started to use <a target="_blank" href="https://feedhive.io">FeedHive</a> to help me grow an audience on Twitter. Recently, I wanted to check how my Twitter followers have grown over time. Unfortunately, <a target="_blank" href="https://analytics.twitter.com">Twitter Analytics</a> only provides data from the last 30 days. So I decided to develop a simple serverless API to fetch and store my follower count each month.</p>
<h2 id="tech-stacklesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatertech-stack">Tech Stack<a class="post-section-overview" href="#tech-stack"></a></h2>
<p>As I already use <a target="_blank" href="https://aws.amazon.com/amplify/">AWS Amplify</a> for some private APIs, I wanted to reuse this framework for this new project.</p>
<p><a target="_blank" href="/static/ac5cc5ac00a66e35d91e1333d1d2ebf2/0f98f/amplify-twitter-architecture.jpg"><img src="https://www.mokkapps.de/static/ac5cc5ac00a66e35d91e1333d1d2ebf2/15ec7/amplify-twitter-architecture.jpg" alt="AWS Amplify Architecture" /></a></p>AWS Amplify Architecture<p></p>
<p>For this new project I need the following components:</p>
<ul>
<li><a target="_blank" href="https://reactjs.org/">React</a> for the frontend web application which will fetch the data from my serverless API</li>
<li><a target="_blank" href="https://aws.amazon.com/api-gateway/">AWS API Gateway</a> which provides traffic management, CORS support, authorization and access control, throttling, monitoring, and API version management for the new API</li>
<li><a target="_blank" href="https://aws.amazon.com/lambda/">AWS Lambda</a> with <a target="_blank" href="https://nodejs.org/">Node.js</a> that fetches the follower count from <a target="_blank" href="https://developer.twitter.com/en/docs/twitter-api">Twitter API</a></li>
<li><a target="_blank" href="https://aws.amazon.com/de/dynamodb/">AWS DynamoDB</a> which is a NoSQL database and which will store the follower count</li>
</ul>
<h2 id="fetching-follower-count-from-backendlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterfetching-follower-count-from-backend">Fetching follower count from backend<a class="post-section-overview" href="#fetching-follower-count-from-backend"></a></h2>
<p>The first step is to add a new Node.js REST API to our Amplify application that provides a <code>/twitter</code> endpoint which is triggered on a recurring schedule. In my case, it will be on every 1st day of the month. The <a target="_blank" href="https://docs.amplify.aws/guides/api-rest/node-api/q/platform/js/">official documentation</a> will help you to set up such a new REST API.</p>
<p>To be able to fetch the follower count from Twitter API I decided to use <a target="_blank" href="https://github.com/FeedHive/twitter-api-client">FeedHive’s Twitter Client</a>. This library needs four secrets to be able to access Twitter API. We will store them in the <a target="_blank" href="https://aws.amazon.com/secrets-manager/">AWS Secret Manager</a>, my article <a target="_blank" href="https://www.mokkapps.de/blog/how-to-use-environment-variables-to-store-secrets-in-aws-amplify-backend/">“How to Use Environment Variables to Store Secrets in AWS Amplify Backend”</a> will guide you through this process.</p>
<p>After the API is created and pushed to the cloud, we can write the basic functionality to fetch the Twitter followers inside our AWS Lambda function:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> twitterApiClient = <span class="hljs-built_in">require</span>(<span class="hljs-string">'twitter-api-client'</span>);
<span class="hljs-keyword">const</span> AWS = <span class="hljs-built_in">require</span>(<span class="hljs-string">'aws-sdk'</span>);

<span class="hljs-keyword">const</span> twitterUsername = <span class="hljs-string">'yourTwitterUsername'</span>;
<span class="hljs-keyword">const</span> secretsManager = <span class="hljs-keyword">new</span> AWS.SecretsManager();
<span class="hljs-keyword">const</span> responseHeaders = {
  <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>,
  <span class="hljs-string">'Access-Control-Allow-Headers'</span>:
    <span class="hljs-string">'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'</span>,
  <span class="hljs-string">'Access-Control-Allow-Methods'</span>: <span class="hljs-string">'OPTIONS,POST'</span>,
  <span class="hljs-string">'Access-Control-Allow-Credentials'</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-string">'Access-Control-Allow-Origin'</span>: <span class="hljs-string">'*'</span>,
  <span class="hljs-string">'X-Requested-With'</span>: <span class="hljs-string">'*'</span>,
};

<span class="hljs-built_in">exports</span>.handler = <span class="hljs-keyword">async</span> event =&gt; {
  <span class="hljs-keyword">const</span> secretData = <span class="hljs-keyword">await</span> secretsManager
    .getSecretValue({ <span class="hljs-attr">SecretId</span>: <span class="hljs-string">'prod/twitterApi/twitter'</span> })
    .promise();
  <span class="hljs-keyword">const</span> secretValues = <span class="hljs-built_in">JSON</span>.parse(secretData.SecretString);

  <span class="hljs-keyword">const</span> twitterClient = <span class="hljs-keyword">new</span> twitterApiClient.TwitterClient({
    <span class="hljs-attr">apiKey</span>: secretValues.TWITTER_API_KEY,
    <span class="hljs-attr">apiSecret</span>: secretValues.TWITTER_API_KEY_SECRET,
    <span class="hljs-attr">accessToken</span>: secretValues.TWITTER_ACCESS_TOKEN,
    <span class="hljs-attr">accessTokenSecret</span>: secretValues.TWITTER_ACCESS_TOKEN_SECRET,
  });

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> twitterClient.accountsAndUsers.usersSearch({
      <span class="hljs-attr">q</span>: twitterUsername,
    });
    <span class="hljs-keyword">const</span> followersCount = response[<span class="hljs-number">0</span>].followers_count;

    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
      <span class="hljs-attr">headers</span>: responseHeaders,
      <span class="hljs-attr">body</span>: followersCount,
    };
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error:'</span>, e);
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
      <span class="hljs-attr">headers</span>: responseHeaders,
      <span class="hljs-attr">body</span>: e.message ? e.message : <span class="hljs-built_in">JSON</span>.stringify(e),
    };
  }
};
</code></pre>
<p>The next step is to add DynamoDB support to be able to store a new follower count and get a list of the stored data.</p>
<p>Therefore, we need to add a new storage to our AWS Amplify application, see <a target="_blank" href="https://docs.amplify.aws/cli/storage/overview/#adding-a-nosql-database">“Adding a NoSQL database”</a> for detailed instructions.</p>
<p>We are adding a NoSQL table that has the following columns:</p>
<ul>
<li><code>id</code>: A unique string identifier for each row as a string</li>
<li><code>follower_count</code>: the current follower count as number</li>
<li><code>data</code>: an ISO timestamp string that represents the time when the follower count was fetched</li>
</ul>
<p>Now, we need to allow our Lambda function to access this storage:</p>
<pre><code class="lang-js">▶ amplify update <span class="hljs-function"><span class="hljs-keyword">function</span>
? <span class="hljs-title">Select</span> <span class="hljs-title">the</span> <span class="hljs-title">Lambda</span> <span class="hljs-title">function</span> <span class="hljs-title">you</span> <span class="hljs-title">want</span> <span class="hljs-title">to</span> <span class="hljs-title">update</span> <span class="hljs-title">twitterfunction</span>
? <span class="hljs-title">Which</span> <span class="hljs-title">setting</span> <span class="hljs-title">do</span> <span class="hljs-title">you</span> <span class="hljs-title">want</span> <span class="hljs-title">to</span> <span class="hljs-title">update</span>? <span class="hljs-title">Resource</span> <span class="hljs-title">access</span> <span class="hljs-title">permissions</span>
? <span class="hljs-title">Select</span> <span class="hljs-title">the</span> <span class="hljs-title">categories</span> <span class="hljs-title">you</span> <span class="hljs-title">want</span> <span class="hljs-title">this</span> <span class="hljs-title">function</span> <span class="hljs-title">to</span> <span class="hljs-title">have</span> <span class="hljs-title">access</span> <span class="hljs-title">to</span>. <span class="hljs-title">storage</span>
? <span class="hljs-title">Storage</span> <span class="hljs-title">has</span> 3 <span class="hljs-title">resources</span> <span class="hljs-title">in</span> <span class="hljs-title">this</span> <span class="hljs-title">project</span>. <span class="hljs-title">Select</span> <span class="hljs-title">the</span> <span class="hljs-title">one</span> <span class="hljs-title">you</span> <span class="hljs-title">would</span> <span class="hljs-title">like</span> <span class="hljs-title">your</span> <span class="hljs-title">Lambda</span> <span class="hljs-title">to</span> <span class="hljs-title">access</span> <span class="hljs-title">twitterdynamo</span>
? <span class="hljs-title">Select</span> <span class="hljs-title">the</span> <span class="hljs-title">operations</span> <span class="hljs-title">you</span> <span class="hljs-title">want</span> <span class="hljs-title">to</span> <span class="hljs-title">permit</span> <span class="hljs-title">on</span> <span class="hljs-title">twitterdynamo</span> <span class="hljs-title">create</span>, <span class="hljs-title">read</span>, <span class="hljs-title">update</span>, <span class="hljs-title">delete</span></span>
</code></pre>
<p>Finally, we can use the <a target="_blank" href="https://github.com/aws/aws-sdk-js">AWS SDK</a> to store and read from DynamoDB:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> twitterApiClient = <span class="hljs-built_in">require</span>(<span class="hljs-string">'twitter-api-client'</span>);
<span class="hljs-keyword">const</span> AWS = <span class="hljs-built_in">require</span>(<span class="hljs-string">'aws-sdk'</span>);
<span class="hljs-keyword">const</span> { <span class="hljs-attr">v4</span>: uuidv4 } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'uuid'</span>);

<span class="hljs-keyword">const</span> secretsManager = <span class="hljs-keyword">new</span> AWS.SecretsManager();

<span class="hljs-keyword">const</span> twitterUsername = <span class="hljs-string">'yourTwitterUsername'</span>;
<span class="hljs-keyword">const</span> responseHeaders = {
  <span class="hljs-string">'Access-Control-Allow-Origin'</span>: <span class="hljs-string">'*'</span>,
  <span class="hljs-comment">// ...</span>
};

<span class="hljs-keyword">const</span> docClient = <span class="hljs-keyword">new</span> AWS.DynamoDB.DocumentClient();<span class="hljs-keyword">let</span> tableName = <span class="hljs-string">'twittertable'</span>;<span class="hljs-keyword">if</span> (process.env.ENV &amp;&amp; process.env.ENV !== <span class="hljs-string">'NONE'</span>) { tableName = <span class="hljs-string">`<span class="hljs-subst">${tableName}</span>-<span class="hljs-subst">${process.env.ENV}</span>`</span>;}<span class="hljs-keyword">const</span> tableParams = { <span class="hljs-attr">TableName</span>: tableName,};<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getStoredFollowers</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`👷 Start scanning stored follower data...`</span>); <span class="hljs-keyword">return</span> docClient.scan({ ...tableParams }).promise();}<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">storeFollowersCount</span>(<span class="hljs-params">followerCount</span>) </span>{ <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`👷 Start storing follower count...`</span>); <span class="hljs-keyword">return</span> docClient .put({ ...tableParams, <span class="hljs-attr">Item</span>: { <span class="hljs-attr">id</span>: uuidv4(), <span class="hljs-attr">follower_count</span>: followerCount, <span class="hljs-attr">date</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString(), }, }) .promise();}
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchFollowerCount</span>(<span class="hljs-params">twitterClient</span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`👷 Start fetching follower count...`</span>);
  <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> twitterClient.accountsAndUsers.usersSearch({
    <span class="hljs-attr">q</span>: twitterUsername,
  });
  <span class="hljs-keyword">return</span> data[<span class="hljs-number">0</span>].followers_count;
}

<span class="hljs-built_in">exports</span>.handler = <span class="hljs-keyword">async</span> event =&gt; {
  <span class="hljs-keyword">const</span> secretData = <span class="hljs-keyword">await</span> secretsManager
    .getSecretValue({ <span class="hljs-attr">SecretId</span>: <span class="hljs-string">'prod/twitterApi/twitter'</span> })
    .promise();
  <span class="hljs-keyword">const</span> secretValues = <span class="hljs-built_in">JSON</span>.parse(secretData.SecretString);

  <span class="hljs-keyword">const</span> twitterClient = <span class="hljs-keyword">new</span> twitterApiClient.TwitterClient({
    <span class="hljs-attr">apiKey</span>: secretValues.TWITTER_API_KEY,
    <span class="hljs-attr">apiSecret</span>: secretValues.TWITTER_API_KEY_SECRET,
    <span class="hljs-attr">accessToken</span>: secretValues.TWITTER_ACCESS_TOKEN,
    <span class="hljs-attr">accessTokenSecret</span>: secretValues.TWITTER_ACCESS_TOKEN_SECRET,
  });

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> followersCount = <span class="hljs-keyword">await</span> fetchFollowerCount(twitterClient);

    <span class="hljs-keyword">await</span> storeFollowersCount(followersCount); <span class="hljs-keyword">const</span> storedFollowers = <span class="hljs-keyword">await</span> getStoredFollowers();
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
      <span class="hljs-attr">headers</span>: responseHeaders,
      <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(storedFollowers.Items),
    };
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error:'</span>, e);
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
      <span class="hljs-attr">headers</span>: responseHeaders,
      <span class="hljs-attr">body</span>: e.message ? e.message : <span class="hljs-built_in">JSON</span>.stringify(e),
    };
  }
};
</code></pre>
<p>A successful API response will have a similar JSON array in its body:</p>
<pre><code class="lang-json">[
    {
        <span class="hljs-attr">"follower_count"</span>: <span class="hljs-number">350</span>,
        <span class="hljs-attr">"date"</span>: <span class="hljs-string">"2021-08-09T11:39:50.885Z"</span>,
        <span class="hljs-attr">"id"</span>: <span class="hljs-string">"9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"</span>
    },
    {
        <span class="hljs-attr">"follower_count"</span>: <span class="hljs-number">380</span>,
        <span class="hljs-attr">"date"</span>: <span class="hljs-string">"2021-09-09T11:39:50.885Z"</span>,
        <span class="hljs-attr">"id"</span>: <span class="hljs-string">"a5a2a894-166b-4672-aefe-cea01c70a01a"</span>
    }
]
</code></pre>
<h2 id="show-data-in-frontendlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatershow-data-in-frontend">Show data in frontend<a class="post-section-overview" href="#show-data-in-frontend"></a></h2>
<p>To be able to show the data in the React frontend I use the <a target="_blank" href="https://recharts.org/en-US">Recharts library</a> which is “a composable charting library built on React components”.</p>
<p>The React component is quite simple and uses the <a target="_blank" href="https://docs.amplify.aws/lib/restapi/fetch/q/platform/js/">AWS Amplify REST API library</a> to fetch the data from our API endpoint:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> { API } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-amplify'</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'recharts'</span>;

<span class="hljs-keyword">const</span> TwitterPage = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> [isLoading, setIsLoading] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [apiError, setApiError] = useState();
  <span class="hljs-keyword">const</span> [followerData, setFollowerData] = useState();

  <span class="hljs-keyword">const</span> triggerEndpoint = <span class="hljs-keyword">async</span> () =&gt; {
    setIsLoading(<span class="hljs-literal">true</span>);

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> API.get(<span class="hljs-string">'twitterapi'</span>, <span class="hljs-string">'/twitter'</span>);
      setFollowerData(
        data.map(<span class="hljs-function">(<span class="hljs-params">d</span>) =&gt;</span> {
          d.followers = d.follower_count;
          d.date = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(d.date).toLocaleDateString();
          <span class="hljs-keyword">return</span> d;
        })
      );
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to trigger Twitter endpoint'</span>, error);
      setApiError(<span class="hljs-built_in">JSON</span>.stringify(error));
    } <span class="hljs-keyword">finally</span> {
      setIsLoading(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Twitter API<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">section</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{triggerEndpoint}</span>&gt;</span>
          Fetch followers
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        {followerData ? (
          <span class="hljs-tag">&lt;<span class="hljs-name">LineChart</span>
            <span class="hljs-attr">width</span>=<span class="hljs-string">{700}</span>
            <span class="hljs-attr">height</span>=<span class="hljs-string">{500}</span>
            <span class="hljs-attr">data</span>=<span class="hljs-string">{followerData}</span>
            <span class="hljs-attr">margin</span>=<span class="hljs-string">{{</span>
              <span class="hljs-attr">top:</span> <span class="hljs-attr">30</span>,
              <span class="hljs-attr">bottom:</span> <span class="hljs-attr">30</span>,
            }}
          &gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">CartesianGrid</span> <span class="hljs-attr">strokeDasharray</span>=<span class="hljs-string">"3 3"</span> /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">XAxis</span> <span class="hljs-attr">dataKey</span>=<span class="hljs-string">"date"</span> /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">YAxis</span> <span class="hljs-attr">dataKey</span>=<span class="hljs-string">"followers"</span> /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Tooltip</span> /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Legend</span> /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Line</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">"monotone"</span>
              <span class="hljs-attr">dataKey</span>=<span class="hljs-string">"followers"</span>
              <span class="hljs-attr">stroke</span>=<span class="hljs-string">"#FC1A20"</span>
              <span class="hljs-attr">activeDot</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">r:</span> <span class="hljs-attr">8</span> }}
            /&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">LineChart</span>&gt;</span>
        ) : null}
        {apiError ? <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"py-4"</span>&gt;</span>{JSON.parse(apiError)}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span> : null}
      <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Layout</span>&gt;</span></span>
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> TwitterPage;
</code></pre>
<p>which results in such a graph:</p>
<p><a target="_blank" href="/static/2e3e7cd0cf879a42f9f2c46ac49338d3/eea4a/twitter-count-graph.jpg"><img src="https://www.mokkapps.de/static/2e3e7cd0cf879a42f9f2c46ac49338d3/15ec7/twitter-count-graph.jpg" alt="Twitter Follower Count Graph" /></a></p>Twitter Follower Count Graph<p></p>
<h2 id="conclusionlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>Using serverless functions it is quite easy and cheap to build a custom solution to track Twitter follower growth over time.</p>
<p>What do you use to track your follower growth? Leave a comment and tell me about your solution.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
]]></content:encoded></item><item><title><![CDATA[Use Git Bisect to Find the Commit That Introduced a Bug]]></title><description><![CDATA[As a developer you know that situation: the code worked like a charm and suddenly there is a bug but you have no idea where and when it was introduced.
If you are working in a big team the chances may be quite high that many commits have been added i...]]></description><link>https://blog.mokkapps.de/use-git-bisect-to-find-the-commit-that-introduced-a-bug</link><guid isPermaLink="true">https://blog.mokkapps.de/use-git-bisect-to-find-the-commit-that-introduced-a-bug</guid><category><![CDATA[Git]]></category><category><![CDATA[version control]]></category><category><![CDATA[Bugs and Errors]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Wed, 07 Jul 2021 06:54:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1625640931168/e2gTnKfys.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a developer you know that situation: the code worked like a charm and suddenly there is a bug but you have no idea where and when it was introduced.</p>
<p>If you are working in a big team the chances may be quite high that many commits have been added in the meantime. So finding the commit where the bug was introduced can become quite nasty.</p>
<p>Luckily, <a target="_blank" href="https://git-scm.com/">Git</a> offers a tool that helps to detect the first bad commit that introduces the bug. It is called “git bisect”.</p>
<h2 id="how-does-it-worklesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterhow-does-it-work">How does it work?<a class="post-section-overview" href="#how-does-it-work"></a></h2>
<p>We need to provide Git Bisect two information to be able to identify</p>
<ol>
<li>A “good” commit where the bug <strong>was not</strong> present.</li>
<li>A “bad” commit where the bug <strong>is</strong> present.</li>
</ol>
<p>This way Git “knows” that the bug has to between the “good” and the “bad” commit. Starting the bisect process will split the range of commits between the “good” and “bad ” commit in half and check out a commit in the middle:</p>
<p><a target="_blank" href="/static/0fc1709e56a44e11201cfe3a3f6276e3/0f98f/git-bisect-1.jpg"><img src="https://www.mokkapps.de/static/0fc1709e56a44e11201cfe3a3f6276e3/15ec7/git-bisect-1.jpg" alt="Git Bisect" /></a></p>
<p>Our task is now to validate the code at this commit. This can be done by compiling, running the application or launching a test case for the given bug. Next, we need to tell Git if the test was “good” or “bad”. Git will simply repeat this process until we’ve singled out the commit that contains the bug.</p>
<p>The used algorithm is called <a target="_blank" href="https://en.wikipedia.org/wiki/Binary_search_algorithm">binary search</a>.</p>
<p><a target="_blank" href="/static/137bf431076246b80e2e3f173fa896ca/0f98f/git-bisect-2.jpg"><img src="https://www.mokkapps.de/static/137bf431076246b80e2e3f173fa896ca/15ec7/git-bisect-2.jpg" alt="Git Bisect - First Round" /></a></p>
<h2 id="practical-examplelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterpractical-example">Practical Example<a class="post-section-overview" href="#practical-example"></a></h2>
<p>Let’s look at how we can run Git Bisect from the command line. First, we need to start the process</p>
<pre><code class="lang-bash">$ git bisect start
</code></pre>
<p>Next step is to provide Git a “good” and “bad” commit. The “bad” commit is often the current state which refers to “HEAD”:</p>
<pre><code class="lang-bash">$ git bisect bad HEAD
</code></pre>
<p>To be able to find “good” commit you need to check out any older revision where you are quite sure that the bug did not exist. After you have checked it out and verified that the bug is not present there, we provide Git the corresponding commit hash :</p>
<pre><code class="lang-bash">$ git bisect good acd72832
</code></pre>
<p>Now we are ready to start the “bisecting” process. Git will check out a commit in the middle of the range between the “good” and “bad” commit we provided:</p>
<pre><code class="lang-bash">Bisecting: 6 revisions left to <span class="hljs-built_in">test</span> after this (roughly 2 step)
[commit_ABC] Added controller
</code></pre>
<p>At this point we need to verify if the bug is still present or not. If it is still present we need to run</p>
<pre><code class="lang-bash">$ git bisect bad
</code></pre>
<p>otherwise we run</p>
<pre><code class="lang-bash">$ git bisect good
</code></pre>
<p>to mark it as “good”.</p>
<p>Depending on the result, Git will again split the original commit range and select either the first or second half. It will again check out a commit in the middle and we need to verify if the bug is present there.</p>
<p>This process is repeated until we’ve successfully singled out the bad commit!</p>
<p>Once we’ve found the culprit, we can end the bisect process by running:</p>
<pre><code class="lang-bash">$ git bisect reset
</code></pre>
<p>Git will then finish the bisect process and take us back to our previous HEAD revision.</p>
<h2 id="conclusionlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>Git Bisect can be a helpful tool to track down a bug. I only use <code>git bisect</code> when I absolutely have no idea where the bug was introduced and I need to search through a lot of potentially unrelated changes.</p>
<p>For more information about Git Bisect take a look at the <a target="_blank" href="https://git-scm.com/docs/git-bisect">official docs</a>.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
]]></content:encoded></item><item><title><![CDATA[My Top React Interview Questions]]></title><description><![CDATA[This article summarizes a list of React interview questions that I would ask candidates and that I get often asked in interviews.
Table of Contents

1. What is React?
2. What are the advantages of React?
3. What are disadvantages of React?
4. What is...]]></description><link>https://blog.mokkapps.de/my-top-react-interview-questions</link><guid isPermaLink="true">https://blog.mokkapps.de/my-top-react-interview-questions</guid><category><![CDATA[React]]></category><category><![CDATA[interview]]></category><category><![CDATA[ReactHooks]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[frontend]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Wed, 30 Jun 2021 07:01:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1625036535514/v3mF6XpB7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This article summarizes a list of React interview questions that I would ask candidates and that I get often asked in interviews.</p>
<h2 id="table-of-contentslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatertable-of-contents">Table of Contents<a class="post-section-overview" href="#table-of-contents"></a></h2>
<ul>
<li><a class="post-section-overview" href="#1-what-is-react">1. What is React?</a></li>
<li><a class="post-section-overview" href="#2-what-are-the-advantages-of-react">2. What are the advantages of React?</a></li>
<li><a class="post-section-overview" href="#3-what-are-disadvantages-of-react">3. What are disadvantages of React?</a></li>
<li><a class="post-section-overview" href="#4-what-is-jsx">4. What is JSX?</a></li>
<li><a class="post-section-overview" href="#5-how-to-pass-data-between-components">5. How to pass data between components?</a></li>
<li><a class="post-section-overview" href="#6-what-are-the-differences-between-functional-and-class-components">6. What are the differences between functional and class components?</a></li>
<li><a class="post-section-overview" href="#7-what-is-the-virtual-dom">7. What is the Virtual DOM?</a></li>
<li><a class="post-section-overview" href="#8-is-the-shadow-dom-the-same-as-the-virtual-dom">8. Is the Shadow DOM the same as the Virtual DOM?</a></li>
<li><a class="post-section-overview" href="#9-what-is-react-fiber">9. What is “React Fiber”?</a></li>
<li><a class="post-section-overview" href="#10-how-does-state-differ-from-props">10. How does state differ from props?</a></li>
<li><a class="post-section-overview" href="#11-what-are-the-differences-between-controlled-and-uncontrolled-components">11. What are the differences between controlled and uncontrolled components?</a></li>
<li><a class="post-section-overview" href="#12-what-are-the-different-lifecycle-methods-in-react">12. What are the different lifecycle methods in React?</a></li>
<li><a class="post-section-overview" href="#13-how-can-you-improve-your-react-apps-performance">13. How can you improve your React app’s performance?</a></li>
<li><a class="post-section-overview" href="#14-what-are-keys-in-react">14. What are keys in React?</a></li>
<li><a class="post-section-overview" href="#15-what-are-higher-order-components">15. What are Higher Order Components?</a></li>
<li><a class="post-section-overview" href="#16-what-are-error-boundaries">16. What are error boundaries?</a></li>
<li><a class="post-section-overview" href="#17-why-hooks-were-introduced">17. Why Hooks were introduced?</a></li>
<li><a class="post-section-overview" href="#18-what-is-the-purpose-of-useeffect-hook">18. What is the purpose of useEffect hook?</a></li>
<li><a class="post-section-overview" href="#19-what-are-synthetic-events-in-react">19. What are synthetic events in React?</a></li>
<li><a class="post-section-overview" href="#20-what-is-the-use-of-refs">20. What is the use of refs?</a></li>
<li><a class="post-section-overview" href="#conclusion">Conclusion</a></li>
</ul>
<h2 id="1-what-is-reactlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater1-what-is-react">1. What is React?<a class="post-section-overview" href="#1-what-is-react"></a></h2>
<p><a target="_blank" href="https://reactjs.org/">React</a> is a “JavaScript library for building user interfaces” which was developed by Facebook in 2011.</p>
<p>It’s the V in the MVC (Model - View -Controller), so it is rather an open-source UI library than a framework.</p>
<h2 id="2-what-are-the-advantages-of-reactlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater2-what-are-the-advantages-of-react">2. What are the advantages of React?<a class="post-section-overview" href="#2-what-are-the-advantages-of-react"></a></h2>
<ul>
<li>Good performance: due to VDOM, see <a target="_blank" href="https://mokkapps.de/blog/my-top-react-interview-questions/#7-what-is-the-virtual-dom">#17</a>.</li>
<li>Easy to learn: with basic JavaScript knowledge you can start building applications. Frameworks like Angular require more knowledge about other technologies and patterns like RxJS, TypeScript, and Dependency Injection.</li>
<li>One-way data flow: this flow is also called “parent to child” or “top to bottom” and prevents errors and facilitates debugging.</li>
<li>Reusable components: Re-using React components in other parts of the code or even in different projects can be done with little or no changes.</li>
<li>Huge community: The community supplies a ton of libraries that can be used to build React applications.</li>
<li>It is very popular among developers.</li>
</ul>
<h2 id="3-what-are-the-disadvantages-of-reactlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater3-what-are-the-disadvantages-of-react">3. What are the disadvantages of React?<a class="post-section-overview" href="#3-what-are-the-disadvantages-of-react"></a></h2>
<ul>
<li>As React provides only the View part of the MVC model you mostly will rely on other technologies in your projects as well. Therefore, every React project might look quite different.</li>
<li>Some people think that JSX is too difficult to grasp and too complex.</li>
<li>Often poor documentation for React and its libraries.</li>
</ul>
<h2 id="4-what-is-jsxlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater4-what-is-jsx">4. What is JSX?<a class="post-section-overview" href="#4-what-is-jsx"></a></h2>
<p>JSX (JavaScript XML) allows us to write HTML inside JavaScript. The <a target="_blank" href="https://reactjs.org/docs/introducing-jsx.html">official docs</a> describe it as “syntax extension to JavaScript”.</p>
<p>React recommends using JSX, but it is also possible to create applications <a target="_blank" href="https://reactjs.org/docs/react-without-jsx.html">without using JSX</a> at all.</p>
<p>A simple JSX example:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> element = <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Hello, world!<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span></span>;
</code></pre>
<h2 id="5-how-to-pass-data-between-componentslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater5-how-to-pass-data-between-components">5. How to pass data between components?<a class="post-section-overview" href="#5-how-to-pass-data-between-components"></a></h2>
<ol>
<li>Use props to pass data from parent to child.</li>
<li>Use callbacks to pass data from child to parent.</li>
<li>Use any of the following methods to pass data among siblings:<ul>
<li>Integrating the methods mentioned above.</li>
<li>Using <a target="_blank" href="https://redux.js.org/">Redux</a>.</li>
<li>Utilizing <a target="_blank" href="https://reactjs.org/docs/context.html#api">React’s Context API</a>.</li>
</ul>
</li>
</ol>
<h2 id="6-what-are-the-differences-between-functional-and-class-componentslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater6-what-are-the-differences-between-functional-and-class-components">6. What are the differences between functional and class components?<a class="post-section-overview" href="#6-what-are-the-differences-between-functional-and-class-components"></a></h2>
<p><a target="_blank" href="https://reactjs.org/docs/hooks-intro.html">Hooks</a> were introduced in React 16.8. In previous versions, functional components were called stateless components and did not provide the same features as class components (e.g., accessing state). Hooks enable functional components to have the same features as class components. There are no plans to remove class components from React.</p>
<p>So let’s take a look at the differences:</p>
<h3 id="declaration-and-propslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterdeclaration-props">Declaration &amp; Props<a class="post-section-overview" href="#declaration--props"></a></h3>
<h4 id="functional-componentlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterfunctional-component">Functional Component<a class="post-section-overview" href="#functional-component"></a></h4>
<p>Functional components are JavaScript functions and therefore can be declared using an arrow function or the <code>function</code> keyword. Props are simply function arguments and can be directly used inside JSX:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> Card = <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
 <span class="hljs-keyword">return</span>(
     <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Title: {props.title}<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span></span>
 )
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Card</span>(<span class="hljs-params">props</span>)</span>{
 <span class="hljs-keyword">return</span>(
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Title: {props.title}<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span></span>
 )
}
</code></pre>
<h4 id="class-componentlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterclass-component">Class component<a class="post-section-overview" href="#class-component"></a></h4>
<p>Class components are declared using the ES6 <code>class</code> keyword. Props need to be accessed using the <code>this</code> keyword:</p>
<pre><code class="lang-js"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Card</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">React</span>.<span class="hljs-title">Component</span></span>{
 <span class="hljs-keyword">constructor</span>(props){
   <span class="hljs-built_in">super</span>(props);
 }

 render(){
   <span class="hljs-keyword">return</span>(
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Title: {this.props.title}<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span></span>
   )
 }
}
</code></pre>
<h3 id="handling-statelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterhandling-state">Handling state<a class="post-section-overview" href="#handling-state"></a></h3>
<h4 id="functional-componentslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterfunctional-components">Functional components<a class="post-section-overview" href="#functional-components"></a></h4>
<p>In functional components we need to use the <code>useState</code> hook to be able to handle state:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> Counter = <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> [counter, setCounter] = useState(<span class="hljs-number">0</span>);

    <span class="hljs-keyword">const</span> increment = <span class="hljs-function">() =&gt;</span> {
        setCounter(++counter);
    }

    <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Count: {counter}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{increment}</span>&gt;</span>Increment Counter<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    )
}
</code></pre>
<h4 id="class-componentslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterclass-components">Class components<a class="post-section-overview" href="#class-components"></a></h4>
<p>It’s not possible to use React Hooks inside class components, therefore state handling is done differently in a class component:</p>
<pre><code class="lang-js"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Counter</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">React</span>.<span class="hljs-title">Component</span> </span>{
    <span class="hljs-keyword">constructor</span>(props){
        <span class="hljs-built_in">super</span>(props);
        <span class="hljs-built_in">this</span>.state = {<span class="hljs-attr">counter</span> : <span class="hljs-number">0</span>};
        <span class="hljs-built_in">this</span>.increment = <span class="hljs-built_in">this</span>.increment.bind(<span class="hljs-built_in">this</span>);
    }

    increment {
        <span class="hljs-built_in">this</span>.setState(<span class="hljs-function">(<span class="hljs-params">prevState</span>) =&gt;</span> {
            <span class="hljs-keyword">return</span> {<span class="hljs-attr">counter</span>: prevState.counter++};
        });
    }

    render() {
    <span class="hljs-keyword">return</span> (
            <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Count: {this.state.counter}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{this.increment}</span>&gt;</span>Increment Counter<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
        )
    }
}
</code></pre>
<h2 id="7-what-is-the-virtual-domlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater7-what-is-the-virtual-dom">7. What is the Virtual DOM?<a class="post-section-overview" href="#7-what-is-the-virtual-dom"></a></h2>
<p>The <a target="_blank" href="https://reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom">Virtual DOM (VDOM)</a> is a lightweight JavaScript object and it contains a copy of the real DOM.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Real DOM</td><td>Virtual DOM</td></tr>
</thead>
<tbody>
<tr>
<td>Slow &amp; expensive DOM manipulation</td><td>Fast &amp; inexpensive DOM manipulation</td></tr>
<tr>
<td>Allows direct updates from HTML</td><td>It cannot be used to update HTML directly</td></tr>
<tr>
<td>Wastes too much memory</td><td>Less memory consumption</td></tr>
</tbody>
</table>
</div><h2 id="8-is-the-shadow-dom-the-same-as-the-virtual-domlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater8-is-the-shadow-dom-the-same-as-the-virtual-dom">8. Is the Shadow DOM the same as the Virtual DOM?<a class="post-section-overview" href="#8-is-the-shadow-dom-the-same-as-the-virtual-dom"></a></h2>
<p>No, they are different.</p>
<p>The <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM">Shadow DOM</a> is a browser technology designed primarily for scoping variables and CSS in web components.</p>
<p>The virtual DOM is a concept implemented by libraries in JavaScript on top of browser APIs.</p>
<h2 id="9-what-is-react-fiberlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater9-what-is-react-fiber">9. What is “React Fiber”?<a class="post-section-overview" href="#9-what-is-react-fiber"></a></h2>
<p>Fiber is the new reconciliation engine in React 16.</p>
<p>Its headline feature is incremental rendering: the ability to split rendering work into chunks and spread it out over multiple frames.</p>
<p><a target="_blank" href="https://github.com/acdlite/react-fiber-architecture">Read more</a>.</p>
<h2 id="10-how-does-state-differ-from-propslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater10-how-does-state-differ-from-props">10. How does state differ from props?<a class="post-section-overview" href="#10-how-does-state-differ-from-props"></a></h2>
<p>Both props and state are plain JavaScript objects.</p>
<p>Props (short for “properties”) is an object of arbitrary inputs that are passed to a component by its parent component.</p>
<p>State are variables that are initialized and managed by the component and change over the lifetime of a specific instance of this component.</p>
<p><a target="_blank" href="https://kentcdodds.com/blog/props-vs-state">This article from Kent C. Dodds</a> provides a more detailed explanation.</p>
<h2 id="11-what-are-the-differences-between-controlled-and-uncontrolled-componentslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater11-what-are-the-differences-between-controlled-and-uncontrolled-components">11. What are the differences between controlled and uncontrolled components?<a class="post-section-overview" href="#11-what-are-the-differences-between-controlled-and-uncontrolled-components"></a></h2>
<p>The value of an input element in a controlled React component is controlled by React.</p>
<p>The value of an input element in an uncontrolled React component is controlled by the DOM.</p>
<h2 id="12-what-are-the-different-lifecycle-methods-in-reactlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater12-what-are-the-different-lifecycle-methods-in-react">12. What are the different lifecycle methods in React?<a class="post-section-overview" href="#12-what-are-the-different-lifecycle-methods-in-react"></a></h2>
<p>React class components provide these lifecycle methods:</p>
<ul>
<li><code>componentDidMount()</code>: Runs after the component output has been rendered to the DOM.</li>
<li><code>componentDidUpdate()</code>: Runs immediately after updating occurs.</li>
<li><code>componentWillUnmount()</code>: Runs after the component is unmounted from the DOM and is used to clear up the memory space.</li>
</ul>
<p>There exist some other <a target="_blank" href="https://reactjs.org/docs/react-component.html#legacy-lifecycle-methods">rarely used</a> and <a target="_blank" href="https://reactjs.org/docs/react-component.html#legacy-lifecycle-methods">legacy</a> lifecycle methods.</p>
<p>Hooks are used in functional components instead of the above-mentioned lifecycle methods. The Effect Hook <code>useEffect</code> adds, for example, the ability to perform side effects and provides the same functionality as <code>componentDidMount</code>, <code>componentDidUpdate</code>, and <code>componentWillUnmount</code>.</p>
<h2 id="13-how-can-you-improve-your-react-apps-performancelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater13-how-can-you-improve-your-react-apps-performance">13. How can you improve your React app’s performance?<a class="post-section-overview" href="#13-how-can-you-improve-your-react-apps-performance"></a></h2>
<ul>
<li>Use <a target="_blank" href="https://reactjs.org/docs/react-api.html#reactpurecomponent">React.PureComponent</a> which is a base class like <code>React.Component</code> but it provides in some cases a performance boost if its <code>render()</code> function renders the same result given the same props and state.</li>
<li>Use <a target="_blank" href="https://reactjs.org/docs/hooks-reference.html#usememo">useMemo Hook</a> to memoize functions that perform expensive calculations on every render. It will only recompute the memoized value if one of the dependencies (that are passed to the Hook) has changed.</li>
<li>State colocation is a process that moves the state as close to where you need it. Some React applications have a lot of unnecessary state in their parent component which makes the code harder to maintain and leads to a lot of unnecessary re-renders. <a target="_blank" href="https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster">This article</a> provides a detailed explanation about state colocation.</li>
<li>Lazy load your components to reduce the load time of your application. React <a target="_blank" href="https://reactjs.org/docs/react-api.html#suspense">Suspense</a> can be used to lazy load components.</li>
</ul>
<h2 id="14-what-are-keys-in-reactlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater14-what-are-keys-in-react">14. What are keys in React?<a class="post-section-overview" href="#14-what-are-keys-in-react"></a></h2>
<p>React needs keys to be able to identify which elements were changed, added, or removed. Each item in an array needs to have a key that provides a stable identity.</p>
<p>It’s not recommended to use indexes for keys if the order of items may change as it can have a negative impact on the performance and may cause state issues. React will use indexes as keys if you do not assign an explicit key to list items.</p>
<p>Check out Robin Pokorny’s article for an <a target="_blank" href="https://medium.com/@robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318">in-depth explanation of the negative impacts of using an index as a key</a>. Here is another <a target="_blank" href="https://reactjs.org/docs/reconciliation.html#recursing-on-children">in-depth explanation about why keys are necessary</a> if you’re interested in learning more.</p>
<h2 id="15-what-are-higher-order-componentslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater15-what-are-higher-order-components">15. What are Higher Order Components?<a class="post-section-overview" href="#15-what-are-higher-order-components"></a></h2>
<p>A <a target="_blank" href="https://reactjs.org/docs/higher-order-components.html#use-hocs-for-cross-cutting-concerns">higher-order component (HOC)</a> is a function that takes a component and returns a new component.</p>
<p>They are an advanced technique in React for reusing component logic and they are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> EnhancedComponent = higherOrderComponent(WrappedComponent);
</code></pre>
<p>Whereas a component transforms props into UI, a higher-order component transforms a component into another component.</p>
<h2 id="16-what-are-error-boundarieslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater16-what-are-error-boundaries">16. What are error boundaries?<a class="post-section-overview" href="#16-what-are-error-boundaries"></a></h2>
<p>React 16 introduced a new concept of an “error boundary”.</p>
<p><a target="_blank" href="https://reactjs.org/docs/error-boundaries.html#gatsby-focus-wrapper">Error boundaries</a> are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.</p>
<h2 id="17-why-hooks-were-introducedlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater17-why-hooks-were-introduced">17. Why Hooks were introduced?<a class="post-section-overview" href="#17-why-hooks-were-introduced"></a></h2>
<p>Hooks solve a wide variety of seemingly unconnected problems in React that were encountered by Facebook over five years of writing and maintaining tens of thousands of components:</p>
<ul>
<li>Hooks allow you to reuse stateful logic without changing your component hierarchy.</li>
<li>Hooks let you split one component into smaller functions based on what pieces are related (such as setting up a subscription or fetching data).</li>
<li>Hooks let you use more of React’s features without classes.</li>
<li>It removed the complexity of dealing with the <code>this</code> keyword inside class components.</li>
</ul>
<p><a target="_blank" href="https://reactjs.org/docs/hooks-intro.html#motivation">Read more</a></p>
<h2 id="18-what-is-the-purpose-of-useeffect-hooklesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater18-what-is-the-purpose-of-useeffect-hook">18. What is the purpose of useEffect hook?<a class="post-section-overview" href="#18-what-is-the-purpose-of-useeffect-hook"></a></h2>
<p>The <a target="_blank" href="https://reactjs.org/docs/hooks-reference.html#useeffect">Effect hook</a> lets us perform side effects in functional components. It helps us to avoid redundant code in different lifecycle methods of a class component. It helps to group related code.</p>
<h2 id="19-what-are-synthetic-events-in-reactlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater19-what-are-synthetic-events-in-react">19. What are synthetic events in React?<a class="post-section-overview" href="#19-what-are-synthetic-events-in-react"></a></h2>
<p><a target="_blank" href="https://reactjs.org/docs/events.html">SyntheticEvent</a> is a cross-browser wrapper around the browser’s native event. It has the same API as the browser’s native event, including <code>stopPropagation()</code> and `preventDefault(), except the events work identically across all browsers.</p>
<h2 id="20-what-is-the-use-of-refslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreater20-what-is-the-use-of-refs">20. What is the use of refs?<a class="post-section-overview" href="#20-what-is-the-use-of-refs"></a></h2>
<p>A <a target="_blank" href="https://reactjs.org/docs/glossary.html#refs">Ref</a> is a special attribute that can be attached to any component. It can be an object created by <code>React.createRef()</code>, a callback function or a string (in legacy API).</p>
<p>To get direct access to a DOM element or component instance you can use ref attribute as a callback function. The function receives the underlying DOM element or class instance (depending on the type of element) as its argument.</p>
<p>In most cases, refs should be used sparingly.</p>
<h2 id="conclusionlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>I hope this list of React interview questions will help you to get your next React position. Leave me a comment if you know any other important React interview questions.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
<p>If you are looking for more interview questions you should take a look at this <a target="_blank" href="https://github.com/sudheerj/reactjs-interview-questions">list of top 500 React interview questions &amp; answers</a>.</p>
]]></content:encoded></item><item><title><![CDATA[How to Use Environment Variables to Store Secrets in AWS Amplify Backend]]></title><description><![CDATA[The twelve-factor app is a known methodology for building software-as-a-service apps. One factor describes that an application’s configuration should be stored in the environment and not in the code to enforce a strict separation of config from code....]]></description><link>https://blog.mokkapps.de/how-to-use-environment-variables-to-store-secrets-in-aws-amplify-backend</link><guid isPermaLink="true">https://blog.mokkapps.de/how-to-use-environment-variables-to-store-secrets-in-aws-amplify-backend</guid><category><![CDATA[AWS]]></category><category><![CDATA[backend]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Wed, 19 May 2021 08:31:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1621413078069/_fLptk4kP.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The <a target="_blank" href="https://12factor.net/">twelve-factor app</a> is a known methodology for building software-as-a-service apps. One factor describes that an application’s configuration should be stored in the environment and not in the code to enforce a strict separation of config from code.</p>
<p>In this article, I want to demonstrate how you can add sensitive and insensitive configuration data to an <a target="_blank" href="https://aws.amazon.com/amplify/">AWS Amplify</a> backend using environment variables and <a target="_blank" href="https://aws.amazon.com/secrets-manager/">AWS Secrets Manager</a>.</p>
<h2 id="table-of-contentslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatertable-of-contents">Table of Contents<a class="post-section-overview" href="#table-of-contents"></a></h2>
<ul>
<li><a class="post-section-overview" href="#types-of-configuration">Types of configuration</a></li>
<li><a class="post-section-overview" href="#init-amplify">Init Amplify</a></li>
<li><a class="post-section-overview" href="#add-insensitive-configuration-data">Add insensitive configuration data</a></li>
<li><a class="post-section-overview" href="#add-sensitive-data-using-aws-secrets-manager">Add sensitive data using AWS Secrets Manager</a></li>
<li><a class="post-section-overview" href="#conclusion">Conclusion</a></li>
</ul>
<h2 id="types-of-configurationlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatertypes-of-configuration">Types of configuration<a class="post-section-overview" href="#types-of-configuration"></a></h2>
<p>There exist many types of configuration data, for example:</p>
<ul>
<li>timeouts </li>
<li>connection strings</li>
<li>external API configurations like URLs and endpoints</li>
<li>caching</li>
<li>hosting configuration for URL, port, or schema</li>
<li>file system paths</li>
<li>framework configuration</li>
<li>libraries configuration</li>
<li>business logic parameters</li>
<li>and many more</li>
</ul>
<p>Apart from the type, configuration data can also be categorized as sensitive or insensitive.</p>
<h3 id="sensitive-vs-insensitivelesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatersensitive-vs-insensitive">Sensitive vs Insensitive<a class="post-section-overview" href="#sensitive-vs-insensitive"></a></h3>
<p>Sensitive configuration data is anything that can be potentially exploited by a third party and therefore must be protected from unauthorized access. Examples of such sensitive data are API keys, usernames, passwords, emails, etc. This data should not be part of your version control. Insensitive configuration data is for example a timeout string for a backend endpoint that can be safely added to the version control.</p>
<h2 id="init-amplifylesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterinit-amplify">Init Amplify<a class="post-section-overview" href="#init-amplify"></a></h2>
<p>You need an AWS account and the Amplify CLI installed and configured to be able to follow the following steps. <a target="_blank" href="https://docs.amplify.aws/start/getting-started/installation/q/integration/js">Check out the official docs</a> to set up the prerequisites.</p>
<p>Let’s start by creating a new Amplify project:</p>
<pre><code>mkdir amplify-env-config-demo
cd amplify-env-config-demo

▶ amplify init
? Enter a name for the project amplifyenvconfigdemo
? Initialize the project <span class="hljs-keyword">with</span> the above configuration? Yes
? <span class="hljs-keyword">Select</span> the <span class="hljs-keyword">authentication</span> method you want <span class="hljs-keyword">to</span> <span class="hljs-keyword">use</span>: AWS profile
? Please <span class="hljs-keyword">choose</span> the profile you want <span class="hljs-keyword">to</span> <span class="hljs-keyword">use</span> <span class="hljs-keyword">default</span>
</code></pre><p>Next, we can add an API which we will use to add some environment configuration.</p>
<pre><code>▶ amplify add api
? Please <span class="hljs-keyword">select</span> <span class="hljs-keyword">from</span> one <span class="hljs-keyword">of</span> the below mentioned services: REST
? Provide a friendly <span class="hljs-keyword">name</span> <span class="hljs-keyword">for</span> your <span class="hljs-keyword">resource</span> <span class="hljs-keyword">to</span> be used <span class="hljs-keyword">as</span> a label <span class="hljs-keyword">for</span> this <span class="hljs-keyword">category</span> <span class="hljs-keyword">in</span> the <span class="hljs-keyword">project</span>: amplifyenvconfigdemoapi
? Provide a <span class="hljs-keyword">path</span> (e.g., /book/{isbn}): /handle
? <span class="hljs-keyword">Choose</span> a Lambda <span class="hljs-keyword">source</span> <span class="hljs-keyword">Create</span> a <span class="hljs-keyword">new</span> Lambda <span class="hljs-keyword">function</span>
? Provide an AWS Lambda <span class="hljs-keyword">function</span> <span class="hljs-keyword">name</span>: amplifyenvconfigdemofunction
? <span class="hljs-keyword">Choose</span> the runtime that you want <span class="hljs-keyword">to</span> <span class="hljs-keyword">use</span>: NodeJS
? <span class="hljs-keyword">Choose</span> the <span class="hljs-keyword">function</span> <span class="hljs-keyword">template</span> that you want <span class="hljs-keyword">to</span> <span class="hljs-keyword">use</span>: Hello World
? <span class="hljs-keyword">Do</span> you want <span class="hljs-keyword">to</span> configure <span class="hljs-keyword">advanced</span> <span class="hljs-keyword">settings</span>? <span class="hljs-keyword">No</span>
? <span class="hljs-keyword">Do</span> you want <span class="hljs-keyword">to</span> edit the <span class="hljs-keyword">local</span> lambda <span class="hljs-keyword">function</span> <span class="hljs-keyword">now</span>? <span class="hljs-keyword">No</span>
? Restrict API <span class="hljs-keyword">access</span> <span class="hljs-keyword">No</span>
? <span class="hljs-keyword">Do</span> you want <span class="hljs-keyword">to</span> <span class="hljs-keyword">add</span> another <span class="hljs-keyword">path</span>? <span class="hljs-keyword">No</span>
</code></pre><p>We create a simple <a target="_blank" href="https://nodejs.org/">Node.js</a> lambda function based on the “Hello World” Amplify template. It will provide a REST API with an endpoint at the path <code>/handle</code>.</p>
<p>Amplify CLI generated the “Hello World” function code at <code>amplify/backend/function/amplifyenvconfigdemofunction/src/index.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-built_in">exports</span>.handler = <span class="hljs-keyword">async</span> event =&gt; {
  <span class="hljs-comment">// TODO implement</span>
  <span class="hljs-keyword">const</span> response = {
    <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
    <span class="hljs-comment">// Uncomment below to enable CORS requests</span>
    <span class="hljs-comment">// headers: {</span>
    <span class="hljs-comment">// "Access-Control-Allow-Origin": "*",</span>
    <span class="hljs-comment">// "Access-Control-Allow-Headers": "*"</span>
    <span class="hljs-comment">// },</span>
    <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">'Hello from Lambda!'</span>),
  };
  <span class="hljs-keyword">return</span> response;
};
</code></pre>
<h2 id="add-insensitive-configuration-datalesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreateradd-insensitive-configuration-data">Add insensitive configuration data<a class="post-section-overview" href="#add-insensitive-configuration-data"></a></h2>
<p>As we now have a running API, we can add some insensitive configuration data as environment variables to our Amplify backend.</p>
<p>Therefore, we need to modify the <code>amplify/backend/function/amplifyenvconfigdemofunction/amplifyenvconfigdemofunction-cloudformation-template.json</code> file. It includes a <code>Parameters</code> object where we can add a new environment variable. In our case we want to add a string variable that can be accessed with the key <code>MyEnvVariableKey</code> and has the value <code>my-environment-variable</code>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"AWSTemplateFormatVersion"</span>: <span class="hljs-string">"2010-09-09"</span>,
  <span class="hljs-attr">"Description"</span>: <span class="hljs-string">"Lambda Function resource stack creation using Amplify CLI"</span>,
  <span class="hljs-attr">"Parameters"</span> : {
    ...
    <span class="hljs-attr">"env"</span>: {
      <span class="hljs-attr">"Type"</span>: <span class="hljs-string">"String"</span>
    },
    <span class="hljs-attr">"s3Key"</span>: {
      <span class="hljs-attr">"Type"</span>: <span class="hljs-string">"String"</span>
    },
    <span class="hljs-attr">"MyEnvVariableKey"</span> : { <span class="hljs-attr">"Type"</span> : <span class="hljs-string">"String"</span>, <span class="hljs-attr">"Default"</span> : <span class="hljs-string">"my-environment-variable"</span> } },
  ...
}
</code></pre>
<p>We also need to modify the <code>Resources &gt; Environment &gt; Variables</code> object in this file to be able to map our new environment key to a variable that is attached to the global <code>process.env</code> variable and is injected by the Node.js runtime:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"Resources"</span>: {
    <span class="hljs-attr">"Environment"</span>: {
      <span class="hljs-attr">"Variables"</span>: {
        <span class="hljs-attr">"ENV"</span>: {
          <span class="hljs-attr">"Ref"</span>: <span class="hljs-string">"env"</span>
        },
        <span class="hljs-attr">"REGION"</span>: {
          <span class="hljs-attr">"Ref"</span>: <span class="hljs-string">"AWS::Region"</span>
        },
        <span class="hljs-attr">"MY_ENV_VAR"</span>: { <span class="hljs-attr">"Ref"</span>: <span class="hljs-string">"MyEnvVariableKey"</span> } }
    }  
  }
}
</code></pre>
<p>Finally, we need to run <code>amplify push</code> to build all of our local backend resources and provision them in the cloud.</p>
<p>Now we can access this variable in our lambda function by accessing the global <code>process.env</code> variable:</p>
<pre><code class="lang-js"><span class="hljs-built_in">exports</span>.handler = <span class="hljs-keyword">async</span> event =&gt; {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'MY_ENV_VAR'</span>, process.env.MY_ENV_VAR);
  <span class="hljs-keyword">const</span> response = {
    <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
    <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">'Hello from Lambda!'</span>),
  };
  <span class="hljs-keyword">return</span> response;
};
</code></pre>
<h2 id="add-sensitive-data-using-aws-secrets-managerlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreateradd-sensitive-data-using-aws-secrets-manager">Add sensitive data using AWS Secrets Manager<a class="post-section-overview" href="#add-sensitive-data-using-aws-secrets-manager"></a></h2>
<p>AWS provides the <a target="_blank" href="https://aws.amazon.com/secrets-manager/">AWS Secrets Manager</a> that helps to “protect secrets needed to access your applications, services, and IT resources”. We will use this service to be able to access sensitive data from our backend.</p>
<p>First, we need to click on “Store a new secret” to create a new secret:</p>
<p><a target="_blank" href="/static/a17d843a6b4ad7b6f85fe61f34070a61/0f98f/aws-secrets-manager-store-new-secret.jpg"><img src="https://www.mokkapps.de/static/a17d843a6b4ad7b6f85fe61f34070a61/15ec7/aws-secrets-manager-store-new-secret.jpg" alt="Store new secret" /></a></p>Store new secret<p></p>
<p>Next, we click “Other type of secret” and enter key and value of our secret in the corresponding “Secret key/value” inputs:</p>
<p><a target="_blank" href="/static/e68f8bbf03db69ff21cefe4b68709cef/0f98f/aws-secrets-manager-type.jpg"><img src="https://www.mokkapps.de/static/e68f8bbf03db69ff21cefe4b68709cef/15ec7/aws-secrets-manager-type.jpg" alt="Secret type" /></a></p>Secret type<p></p>
<p>It is possible to add multiple key/value pairs to a secret. A new pair can be added by clicking the ”+ Add row” button.</p>
<p>In the next screen we need to add a name and some other optional information to our secret:</p>
<p><a target="_blank" href="/static/015276392d16106d3012812e209405ca/0f98f/aws-secrets-manager-name-and-description.jpg"><img src="https://www.mokkapps.de/static/015276392d16106d3012812e209405ca/15ec7/aws-secrets-manager-name-and-description.jpg" alt="Secret name and description" /></a></p>Secret name and description<p></p>
<p>Let’s finish the wizard by skipping all the following screens by clicking the “Next” button.</p>
<p>Now we can open the secret and inspect its values inside the AWS Secrets Manager:</p>
<p><a target="_blank" href="/static/6dff6e152324bf201457d519f1c771d9/0f98f/aws-secrets-manager-secret-details.jpg"><img src="https://www.mokkapps.de/static/6dff6e152324bf201457d519f1c771d9/15ec7/aws-secrets-manager-secret-details.jpg" alt="Secret details" /></a></p>Secret details<p></p>
<p>We need to copy the “Secret ARN” value as we need to add a new configuration object in our Cloudformation configuration file <code>amplifyenvconfigdemofunction-cloudformation-template.json</code>:</p>
<pre><code class="lang-json"><span class="hljs-string">"lambdaexecutionpolicy"</span>: {
  <span class="hljs-attr">"DependsOn"</span>: [<span class="hljs-string">"LambdaExecutionRole"</span>],
  <span class="hljs-attr">"Type"</span>: <span class="hljs-string">"AWS::IAM::Policy"</span>,
  <span class="hljs-attr">"Properties"</span>: {
  <span class="hljs-attr">"PolicyName"</span>: <span class="hljs-string">"lambda-execution-policy"</span>,
  <span class="hljs-attr">"Roles"</span>: [{ <span class="hljs-attr">"Ref"</span>: <span class="hljs-string">"LambdaExecutionRole"</span> }],
  <span class="hljs-attr">"PolicyDocument"</span>: {
    <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
    <span class="hljs-attr">"Statement"</span>: [
        { <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>, <span class="hljs-attr">"Action"</span>: [<span class="hljs-string">"secretsmanager:GetSecretValue"</span>], <span class="hljs-attr">"Resource"</span>: { <span class="hljs-attr">"Fn::Sub"</span>: [<span class="hljs-string">"arn:aws:secretsmanager:${region}:${account}:secret:key_id"</span>, { <span class="hljs-attr">"region"</span>: { <span class="hljs-attr">"Ref"</span>: <span class="hljs-string">"AWS::Region"</span> }, <span class="hljs-attr">"account"</span>: { <span class="hljs-attr">"Ref"</span>: <span class="hljs-string">"AWS::AccountId"</span> } }] } } ]
    }
  }
}
</code></pre>
<p>Again, we need to run <code>amplify push</code> to build all of our local backend resources and provision them in the cloud.</p>
<p>Now we need to add some JavaScript code to be able to access the secret inside our Node.js lambda function.</p>
<p>First, we need to add the AWS SDK to <code>amplify/backend/function/amplifyenvconfigdemofunction/src/package.json</code> by running:</p>
<pre><code><span class="hljs-built_in">npm</span> install aws-sdk
</code></pre><p>Then we can use the <code>SecretsManager</code> to get our secret values by passing the “Secret name” which we defined in the AWS Secrets Manager:</p>
<pre><code class="lang-j">const AWS = require('aws-sdk');const secretsManager = new AWS.SecretsManager();
exports.handler = async event =&gt; {
  console.log('MY_ENV_VAR', process.env.MY_ENV_VAR);

  const secretData = await secretsManager .getSecretValue({ SecretId: 'dev/demoSecret' }) .promise(); const secretValues = JSON.parse(secretData.SecretString); console.log('DEMO_API_KEY', secretValues.DEMO_API_KEY);
  const response = {
    statusCode: 200,
    body: JSON.stringify('Hello from Lambda!'),
  };
  return response;
};
</code></pre>
<h2 id="conclusionlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>In this article, I demonstrated how you can add sensitive and insensitive environment configuration to your AWS Amplify backend. You can also watch <a target="_blank" href="https://www.youtube.com/watch?v=T3vy3ksa4oc">this video from Nader Dabit</a> if you prefer a visual tutorial.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
]]></content:encoded></item><item><title><![CDATA[Build and Deploy a Serverless GraphQL React App Using AWS Amplify]]></title><description><![CDATA[Recently I recognized that some SaaS (Software as a Service) products use AWS Amplify which helps them to build serverless full-stack applications. I think serverless computing will be the future of apps and software. Therefore, I wanted to gather so...]]></description><link>https://blog.mokkapps.de/build-and-deploy-a-serverless-graphql-react-app-using-aws-amplify</link><guid isPermaLink="true">https://blog.mokkapps.de/build-and-deploy-a-serverless-graphql-react-app-using-aws-amplify</guid><category><![CDATA[React]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Michael Hoffmann]]></dc:creator><pubDate>Fri, 07 May 2021 09:23:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1620379568147/JT0tTvkqs.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently I recognized that some SaaS (Software as a Service) products use <a target="_blank" href="https://aws.amazon.com/amplify/">AWS Amplify</a> which helps them to build serverless full-stack applications. I think <a target="_blank" href="https://www.cloudflare.com/learning/serverless/why-use-serverless/">serverless computing</a> will be the future of apps and software. Therefore, I wanted to gather some hands-on experience, and I built a serverless application using AWS Amplify that uses React as frontend framework and GraphQL as backend API.</p>
<p>In this article, I want to guide you through the process how to build and deploy such an Amplify application.</p>
<h2 id="table-of-contentslesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatertable-of-contents">Table of Contents<a class="post-section-overview" href="#table-of-contents"></a></h2>
<ul>
<li><a class="post-section-overview" href="#set-up-amplify">Set up Amplify</a></li>
<li><a class="post-section-overview" href="#set-up-fullstack-amplify-project">Set up fullstack Amplify project</a><ul>
<li><a class="post-section-overview" href="#create-react-frontend">Create React frontend</a></li>
<li><a class="post-section-overview" href="#initialize-amplify">Initialize Amplify</a></li>
<li><a class="post-section-overview" href="#create-graphql-api">Create GraphQL API</a></li>
<li><a class="post-section-overview" href="#connect-frontend-to-api">Connect frontend to API</a></li>
<li><a class="post-section-overview" href="#add-authentication">Add authentication</a></li>
<li><a class="post-section-overview" href="#deploy-and-host-app">Deploy and host app</a></li>
</ul>
</li>
<li><a class="post-section-overview" href="#whats-next">What’s next</a></li>
<li><a class="post-section-overview" href="#conclusion">Conclusion</a></li>
</ul>
<h2 id="set-up-amplifylesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterset-up-amplify">Set up Amplify<a class="post-section-overview" href="#set-up-amplify"></a></h2>
<p><a target="_blank" href="https://aws.amazon.com/amplify/">AWS Amplify</a> describes itself as:</p>
<blockquote>
<p>Fastest, easiest way to build mobile and web apps that scale</p>
</blockquote>
<p>Amplify provides tools and services to build scalable full-stack applications powered by <a target="_blank" href="https://aws.amazon.com/">AWS (Amazon Web Services)</a>. With Amplify configuring backends and deploying static web apps is easy. It supports web frameworks like Angular, React, Vue, JavaScript, Next.js, and mobile platforms including iOS, Android, React Native, Ionic, and Flutter.</p>
<p>You’ll need to <a target="_blank" href="https://portal.aws.amazon.com/billing/signup?redirect_url=https%3A%2F%2Faws.amazon.com%2Fregistration-confirmation#/start">create an AWS account</a> to follow the following steps. No worries, after signing up you have access to the AWS Free Tier which does not include any upfront charges or term commitments.</p>
<p>The next step is to install the Amplify Command Line Interface (CLI). In my case I used cURL on macOS:</p>
<pre><code><span class="hljs-attribute">curl</span> -sL https://aws-amplify.github.io/amplify-cli/install | bash &amp;&amp; <span class="hljs-variable">$SHELL</span>
</code></pre><p>Alternatively, you can watch <a target="_blank" href="https://www.youtube.com/watch?v=fWbM5DLh25U">this video</a> to learn how to install and configure the Amplify CLI.</p>
<p>Now we can configure Amplify using the CLI</p>
<pre><code><span class="hljs-attribute">amplify</span> configure
</code></pre><p>which will ask us to sign in to AWS Console. Once we’re signed in, Amplify CLI will ask us to create an <a target="_blank" href="https://aws.amazon.com/iam/">AWS IAM</a> user:</p>
<pre><code><span class="hljs-string">Specify</span> <span class="hljs-string">the</span> <span class="hljs-string">AWS</span> <span class="hljs-string">Region</span>
<span class="hljs-string">?</span> <span class="hljs-attr">region:</span> <span class="hljs-comment"># Your preferred region</span>
<span class="hljs-attr">Specify the username of the new IAM user:</span>
<span class="hljs-string">?</span> <span class="hljs-attr">user name:</span> <span class="hljs-comment"># User name for Amplify IAM user</span>
<span class="hljs-string">Complete</span> <span class="hljs-string">the</span> <span class="hljs-string">user</span> <span class="hljs-string">creation</span> <span class="hljs-string">using</span> <span class="hljs-string">the</span> <span class="hljs-string">AWS</span> <span class="hljs-string">console</span>
</code></pre><p>We’ll be redirected to IAM where we need to finish the wizard and create a user with <code>AdministratorAccess</code> in our account to provision AWS resources. Once the user is created, Amplify CLI will ask us to provide the <code>accessKeyId</code> and <code>secretAccessKey</code> to connect Amplify CLI with our created IAM user:</p>
<pre><code>Enter the access key of the newly created user:
? accessKeyId: <span class="hljs-comment"># YOUR_ACCESS_KEY_ID</span>
? secretAccessKey: <span class="hljs-comment"># YOUR_SECRET_ACCESS_KEY</span>
This would <span class="hljs-keyword">update</span>/<span class="hljs-keyword">create</span> the AWS Profile <span class="hljs-keyword">in</span> your <span class="hljs-keyword">local</span> machine
? Profile <span class="hljs-keyword">Name</span>: <span class="hljs-comment"># (default)</span>

Successfully <span class="hljs-keyword">set</span> up the <span class="hljs-keyword">new</span> user.
</code></pre><h2 id="set-up-full-stack-amplify-projectlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterset-up-full-stack-amplify-project">Set up full-stack Amplify project<a class="post-section-overview" href="#set-up-full-stack-amplify-project"></a></h2>
<p>At this point, we are ready to set up our full-stack project using a <a target="_blank" href="https://reactjs.org/">React</a> application in the frontend and <a target="_blank" href="https://graphql.org/">GraphQL</a> as backend API. We’ll build a Todo CRUD (create, read, update, delete) application that uses this architecture:</p>
<p><a target="_blank" href="/static/a246bf0f24e59432d8bf2c9c242580b6/0f98f/amplify-architecture.jpg"><img src="https://www.mokkapps.de/static/a246bf0f24e59432d8bf2c9c242580b6/15ec7/amplify-architecture.jpg" alt="Amplify demo architecture" /></a></p>
<p>The complete source code of this demo is available at <a target="_blank" href="https://github.com/Mokkapps/amplify-react-graphql-todo-demo/">GitHub</a>.</p>
<h3 id="create-react-frontendlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatercreate-react-frontend">Create React frontend<a class="post-section-overview" href="#create-react-frontend"></a></h3>
<p>Let’s start by creating a new React app using <a target="_blank" href="https://reactjs.org/docs/create-a-new-react-app.html">create-react-app</a>. From our projects directory we run the following commands to create our new React app in a directory called <code>amplify-react-graphql-demo</code> and to navigate into that new directory:</p>
<pre><code>npx <span class="hljs-keyword">create</span>-react-app amplify-react-graphql-demo
cd amplify-react-graphql-demo
</code></pre><p>To start our React app we can run</p>
<pre><code>npm <span class="hljs-keyword">start</span>
</code></pre><p>which will start the development server at <code>http://localhost:3000</code>.</p>
<h3 id="initialize-amplifylesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterinitialize-amplify">Initialize Amplify<a class="post-section-overview" href="#initialize-amplify"></a></h3>
<p>Now it’s time to initialize Amplify in our project. From the root of the project we run</p>
<pre><code>amplify <span class="hljs-keyword">init</span>
</code></pre><p>which will prompt some information about the app:</p>
<pre><code><span class="hljs-string">▶</span> <span class="hljs-string">amplify</span> <span class="hljs-string">init</span>
<span class="hljs-string">?</span> <span class="hljs-string">Enter</span> <span class="hljs-string">a</span> <span class="hljs-string">name</span> <span class="hljs-string">for</span> <span class="hljs-string">the</span> <span class="hljs-string">project</span> <span class="hljs-string">amplifyreactdemo</span>
<span class="hljs-attr">The following configuration will be applied:</span>

<span class="hljs-string">Project</span> <span class="hljs-string">information</span>
<span class="hljs-string">|</span> <span class="hljs-attr">Name:</span> <span class="hljs-string">amplifyreactdemo</span>
<span class="hljs-string">|</span> <span class="hljs-attr">Environment:</span> <span class="hljs-string">dev</span>
<span class="hljs-string">|</span> <span class="hljs-attr">Default editor:</span> <span class="hljs-string">Visual</span> <span class="hljs-string">Studio</span> <span class="hljs-string">Code</span>
<span class="hljs-string">|</span> <span class="hljs-attr">App type:</span> <span class="hljs-string">javascript</span>
<span class="hljs-string">|</span> <span class="hljs-attr">Javascript framework:</span> <span class="hljs-string">react</span>
<span class="hljs-string">|</span> <span class="hljs-attr">Source Directory Path:</span> <span class="hljs-string">src</span>
<span class="hljs-string">|</span> <span class="hljs-attr">Distribution Directory Path:</span> <span class="hljs-string">build</span>
<span class="hljs-string">|</span> <span class="hljs-attr">Build Command:</span> <span class="hljs-string">npm</span> <span class="hljs-string">run-script</span> <span class="hljs-string">build</span>
<span class="hljs-string">|</span> <span class="hljs-attr">Start Command:</span> <span class="hljs-string">npm</span> <span class="hljs-string">run-script</span> <span class="hljs-string">start</span>

<span class="hljs-string">?</span> <span class="hljs-string">Initialize</span> <span class="hljs-string">the</span> <span class="hljs-string">project</span> <span class="hljs-string">with</span> <span class="hljs-string">the</span> <span class="hljs-string">above</span> <span class="hljs-string">configuration?</span> <span class="hljs-literal">Yes</span>
<span class="hljs-string">Using</span> <span class="hljs-string">default</span> <span class="hljs-string">provider</span> <span class="hljs-string">awscloudformation</span>
<span class="hljs-string">?</span> <span class="hljs-attr">Select the authentication method you want to use:</span> <span class="hljs-string">AWS</span> <span class="hljs-string">profile</span>
<span class="hljs-string">?</span> <span class="hljs-attr">Please choose the profile you want to use:</span> <span class="hljs-string">default</span>
</code></pre><p>When our new Amplify project is initialized, the CLI:</p>
<ul>
<li>created a file called <code>aws-exports.js</code> in the src directory that holds all the configuration for the services we create with Amplify</li>
<li>created a top-level directory called <code>amplify</code> that contains our backend definition</li>
<li>modified the <code>.gitignore</code> file and adds some generated files to the ignore list</li>
</ul>
<p>Additionally, a new cloud project is created in the <a target="_blank" href="https://docs.aws.amazon.com/amplify/latest/userguide/welcome.html">AWS Amplify Console</a> that can be accessed by running <code>amplify console</code>. Amplify Console provides two main services: hosting and the Admin UI. More information can be found <a target="_blank" href="https://docs.aws.amazon.com/amplify/latest/userguide/welcome.html">here</a>.</p>
<p>The next step is to install some Amplify libraries:</p>
<pre><code><span class="hljs-built_in">npm</span> install aws-amplify @aws-amplify/ui-react typescript
</code></pre><ul>
<li><code>aws-amplify</code>: the main library for working with Amplify in your apps</li>
<li><code>@aws-amplify/ui-react</code>: includes React specific UI components</li>
<li><code>typescript</code>: we will use <a target="_blank" href="https://www.typescriptlang.org/">TypeScript</a> in some parts of this demo</li>
</ul>
<p>Next, we need to configure Amplify on the client. Therefore, we need to add the following code below the last import in <code>src/index.js</code> :</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> Amplify <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-amplify'</span>;
<span class="hljs-keyword">import</span> awsExports <span class="hljs-keyword">from</span> <span class="hljs-string">'./aws-exports'</span>;
Amplify.configure(awsExports);
</code></pre>
<p>At this point wee have a running React frontend application, Amplify is configured, and we can now add our GraphQL API.</p>
<h3 id="create-graphql-apilesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreatercreate-graphql-api">Create GraphQL API<a class="post-section-overview" href="#create-graphql-api"></a></h3>
<p>We will now create a backend that provides a GraphQL API using AWS AppSync (a managed GraphQL service) that uses Amazon DynamoDB (a NoSQL database).</p>
<p>To add a new API we need to run the following command in our project’s root folder:</p>
<pre><code>▶ amplify add api
? Please <span class="hljs-keyword">select</span> <span class="hljs-keyword">from</span> one <span class="hljs-keyword">of</span> the below mentioned services: GraphQL
? Provide API <span class="hljs-keyword">name</span>: demoapi
? <span class="hljs-keyword">Choose</span> the <span class="hljs-keyword">default</span> authorization <span class="hljs-keyword">type</span> <span class="hljs-keyword">for</span> the API: API <span class="hljs-keyword">key</span>
? Enter a description <span class="hljs-keyword">for</span> the API <span class="hljs-keyword">key</span>:
? <span class="hljs-keyword">After</span> how many <span class="hljs-keyword">days</span> <span class="hljs-keyword">from</span> <span class="hljs-keyword">now</span> the API <span class="hljs-keyword">key</span> should <span class="hljs-keyword">expire</span> (<span class="hljs-number">1</span><span class="hljs-number">-365</span>): <span class="hljs-number">7</span>
? <span class="hljs-keyword">Do</span> you want <span class="hljs-keyword">to</span> configure <span class="hljs-keyword">advanced</span> <span class="hljs-keyword">settings</span> <span class="hljs-keyword">for</span> the GraphQL API: <span class="hljs-keyword">No</span>, I am done.
? <span class="hljs-keyword">Do</span> you have an annotated GraphQL <span class="hljs-keyword">schema</span>? <span class="hljs-keyword">No</span>
? <span class="hljs-keyword">Choose</span> a <span class="hljs-keyword">schema</span> <span class="hljs-keyword">template</span>: Single <span class="hljs-keyword">object</span> <span class="hljs-keyword">with</span> <span class="hljs-keyword">fields</span> (e.g., “Todo” <span class="hljs-keyword">with</span> <span class="hljs-keyword">ID</span>, <span class="hljs-keyword">name</span>, description)
</code></pre><p>After the process finished successfully we can inspect the GraphQL schema at <code>amplify/backend/api/demoapi/schema.graphql</code>:</p>
<pre><code class="lang-graphql">type Todo @model {
  id: ID!
  name: String!
  description: String
}
</code></pre>
<p>The generated Todo type is annotated with a <code>@model</code> directive that is part of the <a target="_blank" href="https://docs.amplify.aws/cli/graphql-transformer/model">GraphQL transform</a> library of Amplify. The library contains multiple directives which can be used for authentication, to define data models, and more. Adding the <code>@model</code> directive will create a database table for this type (in our example a Todo table), the CRUD (create, read, update, delete) schema, and the corresponding GraphQL resolvers.</p>
<p>Now it’s time to deploy our backend:</p>
<pre><code>▶ amplify push
✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category | Resource name | Operation | Provider plugin |
| <span class="hljs-comment">-------- | ------------- | --------- | ----------------- |</span>
| Api | demoapi | <span class="hljs-keyword">Create</span> | awscloudformation |
? <span class="hljs-keyword">Are</span> you sure you want <span class="hljs-keyword">to</span> continue? Yes
? <span class="hljs-keyword">Do</span> you want <span class="hljs-keyword">to</span> generate code <span class="hljs-keyword">for</span> your newly created GraphQL API: Yes
? <span class="hljs-keyword">Choose</span> the code generation <span class="hljs-keyword">language</span> target: typescript
? Enter the <span class="hljs-keyword">file</span> <span class="hljs-keyword">name</span> pattern <span class="hljs-keyword">of</span> graphql queries, mutations <span class="hljs-keyword">and</span> subscriptions: src/graphql<span class="hljs-comment">/**/</span>*.ts
? <span class="hljs-keyword">Do</span> you want <span class="hljs-keyword">to</span> generate/<span class="hljs-keyword">update</span> <span class="hljs-keyword">all</span> possible GraphQL <span class="hljs-keyword">operations</span> - queries, mutations <span class="hljs-keyword">and</span> subscriptions: Yes
? Enter maximum <span class="hljs-keyword">statement</span> <span class="hljs-keyword">depth</span> [increase <span class="hljs-keyword">from</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">if</span> your <span class="hljs-keyword">schema</span> <span class="hljs-keyword">is</span> deeply <span class="hljs-keyword">nested</span>] <span class="hljs-number">2</span>
? Enter the <span class="hljs-keyword">file</span> <span class="hljs-keyword">name</span> <span class="hljs-keyword">for</span> the <span class="hljs-keyword">generated</span> code: src/API.ts
</code></pre><p>After it is finished successfully our GraphQL API is deployed and we can interact with it. To see and interact with the GraphQL API in the AppSync console at any time we can run:</p>
<pre><code>amplify <span class="hljs-built_in">console</span> api
</code></pre><p><a target="_blank" href="/static/a5a105b8113890aea629c1ccbb5de8b9/0f98f/amplify-appsync-api.jpg"><img src="https://www.mokkapps.de/static/a5a105b8113890aea629c1ccbb5de8b9/15ec7/amplify-appsync-api.jpg" alt="AppSync GraphQL API Console" /></a></p>
<p>Alternatively, we can run this command</p>
<pre><code>amplify <span class="hljs-built_in">console</span> api
</code></pre><p>to view the entire app in the Amplify console.</p>
<h3 id="connect-frontend-to-apilesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterconnect-frontend-to-api">Connect frontend to API<a class="post-section-overview" href="#connect-frontend-to-api"></a></h3>
<p>The GraphQL mutations, queries and subscriptions are available at <code>src/graphql</code>. To be able to interact with them we can use the generated <code>src/API.ts</code> file. So we need extend <code>App.js</code> to be able to create, edit and delete Todos via our GraphQL API:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React, { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { API, graphqlOperation } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-amplify/api'</span>;
<span class="hljs-keyword">import</span> { listTodos } <span class="hljs-keyword">from</span> <span class="hljs-string">'./graphql/queries'</span>;
<span class="hljs-keyword">import</span> { createTodo, deleteTodo, updateTodo } <span class="hljs-keyword">from</span> <span class="hljs-string">'./graphql/mutations'</span>;
<span class="hljs-keyword">import</span> TodoList <span class="hljs-keyword">from</span> <span class="hljs-string">'./components/TodoList'</span>;
<span class="hljs-keyword">import</span> CreateTodo <span class="hljs-keyword">from</span> <span class="hljs-string">'./components/CreateTodo'</span>;

<span class="hljs-keyword">const</span> initialState = { <span class="hljs-attr">name</span>: <span class="hljs-string">''</span>, <span class="hljs-attr">description</span>: <span class="hljs-string">''</span> };

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [formState, setFormState] = useState(initialState);
  <span class="hljs-keyword">const</span> [todos, setTodos] = useState([]);
  <span class="hljs-keyword">const</span> [apiError, setApiError] = useState();
  <span class="hljs-keyword">const</span> [isLoading, setIsLoading] = useState(<span class="hljs-literal">false</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    fetchTodos();
  }, []);

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setInput</span>(<span class="hljs-params">key, value</span>) </span>{
    setFormState({ ...formState, [key]: value });
  }

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchTodos</span>(<span class="hljs-params"></span>) </span>{
    setIsLoading(<span class="hljs-literal">true</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> todoData = <span class="hljs-keyword">await</span> API.graphql(graphqlOperation(listTodos)); <span class="hljs-keyword">const</span> todos = todoData.data.listTodos.items;
      setTodos(todos);
      setApiError(<span class="hljs-literal">null</span>);
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed fetching todos:'</span>, error);
      setApiError(error);
    } <span class="hljs-keyword">finally</span> {
      setIsLoading(<span class="hljs-literal">false</span>);
    }
  }

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addTodo</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">if</span> (!formState.name || !formState.description) {
        <span class="hljs-keyword">return</span>;
      }
      <span class="hljs-keyword">const</span> todo = { ...formState };
      setTodos([...todos, todo]);
      setFormState(initialState);
      <span class="hljs-keyword">await</span> API.graphql(graphqlOperation(createTodo, { <span class="hljs-attr">input</span>: todo })); setApiError(<span class="hljs-literal">null</span>);
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed creating todo:'</span>, error);
      setApiError(error);
    }
  }

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">removeTodo</span>(<span class="hljs-params">id</span>) </span>{
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> API.graphql(graphqlOperation(deleteTodo, { <span class="hljs-attr">input</span>: { id } })); setTodos(todos.filter(<span class="hljs-function"><span class="hljs-params">todo</span> =&gt;</span> todo.id !== id));
      setApiError(<span class="hljs-literal">null</span>);
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed deleting todo:'</span>, error);
      setApiError(error);
    }
  }

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onItemUpdate</span>(<span class="hljs-params">todo</span>) </span>{
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> API.graphql( graphqlOperation(updateTodo, { <span class="hljs-attr">input</span>: { <span class="hljs-attr">name</span>: todo.name, <span class="hljs-attr">description</span>: todo.description, <span class="hljs-attr">id</span>: todo.id, }, }) ); setApiError(<span class="hljs-literal">null</span>);
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed updating todo:'</span>, error);
      setApiError(error);
    }
  }

  <span class="hljs-keyword">const</span> errorMessage = apiError &amp;&amp; (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.errorText}</span>&gt;</span>
      {apiError.errors.map(error =&gt; (
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>{error.message}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      ))}
    <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>
  );

  <span class="hljs-keyword">if</span> (isLoading) {
    <span class="hljs-keyword">return</span> <span class="hljs-string">'Loading...'</span>;
  }

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.container}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.heading}</span>&gt;</span>Amplify React &amp; GraphQL Todos<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      {errorMessage}
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.grid}</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">TodoList</span>
          <span class="hljs-attr">todos</span>=<span class="hljs-string">{todos}</span>
          <span class="hljs-attr">onRemoveTodo</span>=<span class="hljs-string">{removeTodo}</span>
          <span class="hljs-attr">onItemUpdate</span>=<span class="hljs-string">{onItemUpdate}</span>
        /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">CreateTodo</span>
          <span class="hljs-attr">description</span>=<span class="hljs-string">{formState.description}</span>
          <span class="hljs-attr">name</span>=<span class="hljs-string">{formState.name}</span>
          <span class="hljs-attr">onCreate</span>=<span class="hljs-string">{addTodo}</span>
          <span class="hljs-attr">onDescriptionChange</span>=<span class="hljs-string">{setInput}</span>
          <span class="hljs-attr">onNameChange</span>=<span class="hljs-string">{setInput}</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<p>The full source code of this demo is available at <a target="_blank" href="https://github.com/Mokkapps/amplify-react-graphql-todo-demo/">GitHub</a>.</p>
<p>The application should show a list of available Todos which can be edited or deleted. Additionally, we have the possibility to create new Todos:</p>
<p><a target="_blank" href="/static/e9d209ce201f8ae6cc7f7d9081971105/0f98f/amplify-frontend-running.jpg"><img src="https://www.mokkapps.de/static/e9d209ce201f8ae6cc7f7d9081971105/15ec7/amplify-frontend-running.jpg" alt="Amplify Frontend Running Locally" /></a></p>
<h3 id="add-authenticationlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreateradd-authentication">Add authentication<a class="post-section-overview" href="#add-authentication"></a></h3>
<p>Amplify uses <a target="_blank" href="https://aws.amazon.com/cognito/">Amazon Cognito</a> as the main authentication provider. We’ll use it to add authentication to our application by adding a login that requires a password and username.</p>
<p>To add authentication we need to run</p>
<pre><code>▶ amplify <span class="hljs-keyword">add</span> auth
<span class="hljs-keyword">Using</span> service: Cognito, provided <span class="hljs-keyword">by</span>: awscloudformation
 The <span class="hljs-keyword">current</span> configured provider <span class="hljs-keyword">is</span> Amazon Cognito.

 <span class="hljs-keyword">Do</span> you want <span class="hljs-keyword">to</span> use the <span class="hljs-keyword">default</span> authentication <span class="hljs-keyword">and</span> <span class="hljs-keyword">security</span> <span class="hljs-keyword">configuration</span>? <span class="hljs-keyword">Default</span> <span class="hljs-keyword">configuration</span>
 <span class="hljs-built_in">Warning</span>: you will <span class="hljs-keyword">not</span> be able <span class="hljs-keyword">to</span> edit these selections.
 How <span class="hljs-keyword">do</span> you want users <span class="hljs-keyword">to</span> be able <span class="hljs-keyword">to</span> sign <span class="hljs-keyword">in</span>? Username
 <span class="hljs-keyword">Do</span> you want <span class="hljs-keyword">to</span> configure advanced settings? <span class="hljs-keyword">No</span>, I am done.
</code></pre><p>and deploy our service by running</p>
<pre><code>amplify <span class="hljs-keyword">push</span>
</code></pre><p>Now we can add the login UI to our frontend. The login flow can easily be handled by using the <code>withAuthenticator</code> wrapper from the <code>@aws-amplify/ui-react</code> package. We just need to adjust our <code>App.js</code> and import <code>withAuthenticator</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { withAuthenticator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-amplify/ui-react'</span>;
</code></pre>
<p>Now we need to wrap the main component with the <code>withAuthenticator</code> wrapper:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> withAuthenticator(App);
</code></pre>
<p>Running <code>npm start</code> will now start the app with an authentication flow allowing users to sign up and sign in:</p>
<p><a target="_blank" href="/static/452e118fd763caa8ce4858c2c16b19c5/0f98f/amplify-login.jpg"><img src="https://www.mokkapps.de/static/452e118fd763caa8ce4858c2c16b19c5/15ec7/amplify-login.jpg" alt="Amplify Login" /></a></p>
<h3 id="deploy-and-host-applesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterdeploy-and-host-app">Deploy and host app<a class="post-section-overview" href="#deploy-and-host-app"></a></h3>
<p>Finally, we want to deploy our app which can be either done manually or via automatic continuous deployment. In this demo I want to deploy it manually and host it as static web app. If you want to use continuous deployment instead, please check out <a target="_blank" href="https://docs.aws.amazon.com/amplify/latest/userguide/multi-environments.html#standard">this official guide</a>.</p>
<p>First, we need to add hosting:</p>
<pre><code>▶ amplify add hosting
? <span class="hljs-keyword">Select</span> the <span class="hljs-keyword">plugin</span> <span class="hljs-keyword">module</span> <span class="hljs-keyword">to</span> <span class="hljs-keyword">execute</span>: Hosting <span class="hljs-keyword">with</span> Amplify Console (<span class="hljs-keyword">Managed</span> hosting <span class="hljs-keyword">with</span> custom domains, Continuous deployment)
? <span class="hljs-keyword">Choose</span> a <span class="hljs-keyword">type</span>: <span class="hljs-keyword">Manual</span> deployment
</code></pre><p>and then we are ready to publish our app:</p>
<pre><code><span class="hljs-attribute">amplify</span> publish
</code></pre><p>After publishing, we can see the app URL where our application is hosted on an `amplifyapp.com domain in our terminal.</p>
<h2 id="whats-nextlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterwhats-next">What’s next<a class="post-section-overview" href="#whats-next"></a></h2>
<p>Amplify provides also a way to run your API locally, <a target="_blank" href="https://docs.amplify.aws/start/getting-started/data-model/q/integration/react#optional-test-your-api">check out this tutorial</a>.</p>
<p>Here are some cool things that you can additionally add to your Amplify application:</p>
<ul>
<li><a target="_blank" href="https://docs.amplify.aws/lib/datastore/getting-started/q/platform/js">DataStore</a> </li>
<li><a target="_blank" href="https://docs.amplify.aws/lib/storage/getting-started/q/platform/js">User File Storage</a> </li>
<li><a target="_blank" href="https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/js">Serverless APIs</a> </li>
<li><a target="_blank" href="https://docs.amplify.aws/lib/analytics/getting-started/q/platform/js">Analytics</a> </li>
<li><a target="_blank" href="https://docs.amplify.aws/lib/predictions/getting-started/q/platform/js">AI/ML</a> </li>
<li><a target="_blank" href="https://docs.amplify.aws/lib/push-notifications/getting-started/q/platform/js">Push Notification</a> </li>
<li><a target="_blank" href="https://docs.amplify.aws/lib/pubsub/getting-started/q/platform/js">PubSub</a> </li>
<li><a target="_blank" href="https://docs.amplify.aws/lib/xr/getting-started/q/platform/js">AR/VR</a></li>
</ul>
<p>Take a look at the <a target="_blank" href="https://docs.amplify.aws">official Amplify docs</a> for further information about the framework.</p>
<h2 id="conclusionlesssvg-aria-hiddentrue-focusablefalse-height16-version11-viewbox0-0-16-16-width16greaterlesspath-fill-ruleevenodd-dm4-9h1v1h4c-15-0-3-169-3-35s255-3-4-3h4c145-0-3-169-3-35-0-141-91-272-2-325v859c58-45-1-127-1-209c10-522-898-4-8-4h4c-98-0-2-122-2-25s3-9-4-9zm9-3h-1v1h1c1-0-2-122-2-25s1398-12-13-12h9c-98-0-2-122-2-25-0-8342-164-1-209v625c-10953-2-184-2-325c6-1131-755-13-9-13h4c145-0-3-169-3-35s145-6-13-6zgreaterlesspathgreaterlesssvggreaterconclusion">Conclusion<a class="post-section-overview" href="#conclusion"></a></h2>
<p>In this article I showed you that building and deploying a full-stack serverless application using AWS Amplify requires a minimum amount of work. Without using such a framework it would be much harder and this way you can focus more on the end product instead of what is happening inside.</p>
<p>If you liked this article, follow me on <a target="_blank" href="https://twitter.com/mokkapps">Twitter</a> to get notified about new blog posts and more content from me.</p>
]]></content:encoded></item></channel></rss>