Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
July 27, 2022 05:11 pm GMT

I Made The Same App 3 Times in 3 Different Frameworks Heres What I Learned.

Angular, Svelte, Solid, React, Vue there are so many frameworks out there. And you probably dealt with the framework dilemma at some point in your career.

Most wanted frameworks.

Some of us pursue React, the most popular framework. And thats completely understandable, because it being famous also implies that its the most demanded skill in the job market.

Next up is Vue, and its second position in the popularity spectrum. Some people prefer Vue for the fact that its so lightweight and yet so scalable, and others love its simplicity and similarity to other frameworks.

Most loved frameworks.

Lets take a leap of faith and change our selection criteria from popularity to likability.

Svelte is the most loved framework, and for good reason. Svelte places strong emphasis on developer happiness because it's so easy to pick up and has a compiler that is more efficient than its peers.

I always wondered how different it would be to build the same app with the same features, but in 3 different frameworks. For each one, I want to answer the following questions:

  • How many errors will I encounter?
  • How much code is there to write?
  • How long does it take to build?

Well be exploring each of the aforementioned frameworks in its own piece, as part of a four-article series.

Well build a website for publicly sharing short-messages, called ithink. Its like Twitter, but with no accounts and you cant delete what you post.

But first, we need an API

If you play around with the CodePen above, youll quickly realize that the database is not fake.

We actually have a back-end side of our application: it handles the storing and retrieving of simple text data. Its also built on Amazons S3 object storage service.

A developer would normally spend hours and hours setting-up a database like this, and its not free either.

Luckily for us, we get a free instance of the AWS S3 Storage when we use Cyclic. No credit card is required.

Deploy to Cyclic

Vue, the lightweight framework

Lets start by exploring the wonders of Vue, the framework that claims itself to be one of the most performant frontend frameworks.

Creating a new project

Its no secret that setup is one of the most important stages in a projects lifecycle. Its the very first step in the journey that transforms an idea into a real-life, working application that runs on a modern computer.

We can all agree that creating a boilerplate from scratch is exhausting. I mean, think about all the folders, files, and code that youd need to keep repeating, forever. No one wants to do all that work!

Luckily, Vue does all the work for us when we run the following command:

npm init vue@latest

It will ask you a bunch of questions so you can create the optimal setup that best fits your project. So go ahead, and accept/reject the following questions:

  • Project name: ithink
  • TypeScript? No
  • JSX Support? No
  • Vue Router? No
  • Pinia? Yes
  • Vitest? No
  • Cypress? No
  • ESLint? No
  • Prettier? No

Well see what each of these means throughout this article.

Next up is installation. This time, you dont have to do anything. Just kick-back, relax, and wait for yarn to do its work! (of course, you can also use npm if thats your preference)

cd ithinkyarn install # or npm install

This command will create a dead-simple boilerplate which comes with some defaults. Go ahead and run yarn dev or npm run dev and open up http://localhost:3000 in your browser. Its already looking great, isnt it?

Youll see a semi-usable application describing Vue, with the addition of some links to its inner-workings.

Unfortunately, most of this stuff is useless to us. Therefore, well erase most of it so that we can continue along this guide:

rm src/components/* src/assets/ src/stores/*echo '' > src/App.vue

Take some time to observe the folder structure. Notice the emphasis that it places on decoupling. Everything is where its supposed to be, making it a very clean and elegant architecture to work on.

tree . index.html package.json public  favicon.ico README.md src  App.vue  components  index.css  main.js  stores vite.config.js yarn.lock

Dont worry if this looks intimidating. Well cover all of it throughout this document.

Configuring SEO

You might have noticed that the title of our document is Vite App, with no immediately obvious way of changing it.

It turns out that this is a great opportunity for us to delve into the first element: index.html. Youve undoubtedly already seen this filename in a billion other places, but probably not in the context of Vue.

So lets dive into index.html, and update some defaults:

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <link rel="icon" href="/favicon.ico" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    - <title>Vite App</title>    + <title>ithink</title>    + <meta name="description" content="Global online messaging, void of any borders." />  </head>  <body>    <div id="app"></div>    <script type="module" src="/src/main.js"></script>  </body></html>

We changed the title and added a description. Hence, quite simply, index.html is the entry-point to our application. Its where you modify the <head> meta-tags, add class-names to the top-most elements such as <body> and <html>, and change the language of your website from English ("en") to say, French ("fr").

Its also impossible to avoid speaking about <div id="app"></div>. As well see in an upcoming section, this is where all-things-Vue are injected. So lets keep on exploring!

Changing the body color

The website were building has an overall dark theme, meaning that the <body> color should be changed from white to dark.

After configuring TailwindCSS, well start adding these changes to our document.

You mustve already created a src/index.css file during your Tailwind configuration, and imported it from src/main.js. So, lets add our custom styles:

/* index.css */@tailwind base;@tailwind utilities;@tailwind components;body {    @apply bg-stone-900;}

As you can see, were using the @apply directive to apply the bg-stone-900 utility to the body. If youre not very Tailwind-savvy, bg-stone-900 is equivalent to the following CSS declaration:

body {    background: #1c1917;}

Adding a header

Its time to delve into our second-most element: App.vue. Its the top-most component of our application.

Whatever you put inside this file will go straight into <div id="app"></div>. Remember it? We mentioned it in the last paragraph of the section that covered index.html. See how it all fits together? Magical!

Folder structure diagram for Vue

And with all that said, lets implement our overly-simplistic header:

<template>    <!-- App.vue -->    <header>        <h1>ithink</h1>        <button>New</button>    </header></template>

As you can see, we kept it pretty simply.

Hey! I hid the Tailwind class-names from the code snipped above. Browse our code repository to see the full code.

Ahem, lets address the elephant in the room. What the heck is <template>?

  • Before we get into that, let me explain a fundamental concept to you.. Web components!

    Web components give developers the ability to create their own tags:

    <profile-card>, <emoji>, <close-button>, <code-editor>

    Its like magic! You get to encapsulate your own HTML, CSS, and JavaScript. Web components are an elegant way to create user-interfaces that resemble native apps. Theyre the future of the web.

    If this sounds cool to you, youre probably wondering how to create these things.

So let me introduce Single File Components! (SFCs)

<script setup>    // Javascript here</script><template>    <!-- HTML here --></template><style scoped>    /* CSS here */</style>

As you can see, SFCs split a components design into three logical parts, and they can come in any order:

  • JavaScript logic: (<script setup>) The brains of your component. Handles state, events, networking, etc
  • HTML document: (<template>) The semantics of your component.
  • CSS declarations: (<style scoped>) The style of your component.

Pretty neat, huh?

Listening to click events

As it currently stands, the new button doesnt do anything, and no one wants a useless button.

So lets change that! We must listen to a click event, and in Vue, events work like this:

<script setup>    function openModal() {        // TODO        alert('button clicked!')    }</script><template>    <button @click="openModal">New</button></template>

Well come back to the openModal function later, when we actually create the modal!

Creating a modal component

Its time to put the src/components folder to good use. This is where well be housing all our custom components.

What you can see in the image above is the New Modal component, written into the NewModal.vue file.

Create a new file at src/components/NewModal.vue. Fill it up with the usual SFC boilerplate:

<script setup>    // NewModal.vue</script><template></template><style scoped></style>

Lets get the markup out of the way: (remember to use the right semantics)

<template>  <div>    <dialog open>      <main>        <form method="dialog">            <label for="content">Content</label>            <textarea id="content"></textarea>            <button value="cancel">Cancel</button>            <button value="default">Post</button>        </form>      </main>      <footer>        <p>Whatever you write will become public.</p>      </footer>    </dialog>  </div></template>

Markup alone is not very useful without the JavaScript logic. Lets attach event handlers to our button elements:

<script setup>    function close() {        // TODO    }</script><template>    ...    <button value="cancel" @click="close">Cancel</button>    <button value="default">Post</button>    ...</template>

Okay, this is great! But notice that when you click on post, the page reloads. This is the default behavior of HTML forms.

Normally, wed change that by calling e.preventDefault. But Vue focuses so much on simplicity that theres a super-convenient shortcut:

<script setup>    function submit() {        // TODO    }</script><template>    ...    <form method="dialog" @submit.prevent="submit">        ...    </form>    ...</template>

Would you look at that! Were able to shorten e.preventDefault() to @submit.prevent. Now, we dont even need to consider the Event object anymore!

Tracking a loading state

The user should know when their application is loading, whether its posting new text to the server or just retrieving it.

Lets create a variable that tracks when our component is sending/receiving data to/from the server. Were gonna be using reactive variables.

  • What is a reactive variable?

    Consider the following scenario:

    let a = 4document.getElementById('container').textContent = a // <p id="container">4</p>

    Say that we updated the value in a to 5. One thing we know for sure is that the <p> element wont change. It will always say 4, unless we explicitely change it.

    Well, we dont have to! With reactivity, the DOM is automatically updated once the related variable is mutated.

    In Vue, reactive variables are created with ref, a function that takes any value and makes it reactive.

    import { ref } from 'vue'const a = ref(4)a.value = 5

    Notice the addition of .value. Its important, because if we did a = 5 wed completely taking away the reactivity of the variable.

    Also, it doesnt matter that a is a constant variable, because were only assigning to its .value property.

    Now look at the HTML:

    <template>    <p>{{ a }}</p></template>

    Vue will replace {{ a }} with its value: a.value, and the double-brackets are not optional.

Okay, lets head right back into the submit logic. Well create a new isLoading reactive variable, which will indicate when our app is in-contact with the server.

import { ref } from 'vue'const isLoading = ref(false)async function submit() {    isLoading.value = true    // TODO: send data to server    isLoading.value = false}

Getting user input from form elements

Our new modal component cant exist without the <textarea> form element. But this raises an important question: how do we keep the javascript code in-sync with the DOM content?

In a nutshell, Vue has a shortcut way of dealing with form elements. And its quite convenient!

<script setup>    import { ref } from 'vue'    const message = ref('')    async function submit() {        // TODO: implement addItem        addItem(message.value)    }</script><template>    ...    <textarea id="content" v-model="message"></textarea></template>

As you can see, message is a reactive variable, so whenever the user types something into textarea, message will be instantly updated. Thats the magic of v-model!

Hey! You may be getting tired from typing .value all the time. Thankfully, Vue has plans to change that.

Making the modal inert while loading

When our app is loading (which is tracked by the isLoading variable that we saw in a previous section), we should disable interaction with it.

This requires us getting access to a DOM element in JavaScript. The pure way of doing that is with body.querySelector(), but this approach is non-reactive. Plus, theres a simpler alternative offered by Vue:

<script setup>    import { ref } from 'vue'    const container = ref(null)</script><template>    <div ref="container"></div></template>

These are called template refs! And although it says null, Vue will fill container with the DOM element <div ref="container"> when its created. It will also go back to null when it disappears.

With this in hand, we can make our component inert while its loading:

<script setup>    import { ref } from 'vue'    const container = ref(null)    async function submit() {        isLoading.value = true;        dialog.value.setAttribute("inert", true);        // TODO: send data to server        dialog.value.removeAttribute("inert");        isLoading.value = false;    }</script><template>    <div ref="container"></div></template><style scoped>dialog[inert] {  @apply filter brightness-90;}</style>

Hey! While theres a simpler way to achieve the above (<div :inert="isLoading"), I just had to create an opportunity to teach you about template refs, which are a pretty important feature in VueJS.

Automatically focusing the <textarea>

When the user opens the NewModal component, we know that their objective is to input into the <textarea>. So, wouldnt it be convenient to save them the hassle of moving the cursor towards that form element?

Lets implement this feature! Counter-intuitively, we cant do this:

<script setup>    import { ref } from 'vue'    const textarea = ref(null)    textarea.value.focus() // this is WRONG!</script><template>    <textarea ref="textarea"></textarea></template>

The above code wont work, because its the same as saying null.focus().

So, how do we tell Vue to only focus the textarea once its available? The answer lies in lifecycle hooks.

Lifecycle diagram for Vue

We want to utilize the mounted hook, which is called right after the component is added to the DOM. Thats when the <textarea> is shown, meaning that its not null:

import { onMounted } from 'vue'onMounted(() => {    textarea.value.focus() // CORRECT! :)})

There are many more lifecycle hooks, and we usually use most of them. However, in our application, the mounted hook was more than enough. But just keep one thing in mind, youll see this concept again and again in every framework.

Closing the modal when clicked outside

Its a pretty common behavior in every modal to close it once the user clicks outside.

In this step, well take a HUGE shortcut. Instead of manually implementing this behavior, we shall use another library to do it for us.

The Vue community is humongous and infinitely helpful, so its not a surprise to find a library like @vueuse: over 200 useful methods that cover almost every possible usecase.

Our interest lies in the [onClickOutside event](https://vueuse.org/core/onClickOutside/), which is triggered every time the user clicks outside a certain DOM element.

After setting up @vueuse, lets add it to our component:

<script setup>    import { ref } from 'vue'    import { onClickOutside } from '@vueuse/core'    // Close dialog when clicked outside    const container = ref(null)    onClickOutside(container, close)    function close() {        // TODO    }</script><template>    <dialog ref="container">...</dialog></template>

Fascinating how in one line of code, were able to implement such a seemingly complicating feature!

Closing the modal

The modal has no way of telling whether its opened or closed, as only its parent App.vue knows such information.

But theres one thing we know to be true, and its that the modal knows when it should be closed:

  • user clicked outside,
  • user submitted,
  • user canceled.

Therefore, we need a way for the child NewModal to communicate with its parent, App. And the solution is quite simple: Events!

With Vue, its super-simple to define our own events, while dictating when they should be emitted, using our own rules.

const emit = defineEmits(['close'])function close() {    emit('close')}

Henceforth, whenever the modal needs to be closed, we emit the event close up into its parent.

Hey! Notice how we didnt import defineEmits. Thats because its a compiler macro, so its always there by default.

Using our custom component

Up until now, you couldnt see NewModal, because it hasnt been added to App.vue yet. So, lets change that:

<script setup>    import NewModal from './components/NewModal.vue'</script><template>    <NewModal /></template>

With the code snippet above, the modal will always be open. So, lets add some toggle magic:

<script setup>    import NewModal from './components/NewModal.vue'    import { ref } from 'vue'    const isModalOpen = ref(false)    function openModal() {        isModalOpen.value = true    }    function closeModal() {        isModalOpen.value = false    }</script><template>    <NewModal v-if="isModalOpen" /></template>

We took advantage of v-if to show/hide the modal.

And now were done with the NewModal component. Pretty simple, huh?

Creating a dynamic list of DOM elements

Its time to delve into a new component, ThoughtList. This one will display a list of items, and we dont know the length of that list before-hand.

Lets create a new file at src/ThoughtList.vue. In Vue, heres how we repeat an element:

<script setup>    import { ref } from 'vue'    const items = ref(['hello', 'world!'])</script><template>    <ul>        <li v-for="item in items">            <p>{{ item }}</p>        </li>    </ul></template>

Surprisingly simple! It might be useful to split this component into two: ThoughtList.vue and ThoughtItem.vue.

So, well change src/ThoughtList.vue to:

<script setup>    import { ref } from 'vue'    import ThoughtItem from './ThoughtItem.vue'    const items = ref(['hello', 'world!'])</script><template>    <ul>        <li v-for="item in items">            <ThoughtItem />        </li>    </ul></template>

and the new ThoughtItem.vue will contain:

<template>  <p>    Hello world!  </p></template>

And of course, dont forget to add it to App.vue:

<script setup>import ThoughtList from "./components/ThoughtList.vue";</script><template>  <main>    <ThoughtList />  </main></template>

Passing text from parent to child

Weve seen how to trigger events from child to parent, and now, its time to flip that formula around.

Currently, our ThoughtItem displays the same text: Hello world!

But we want to show real data, which is held by the parent ThoughtList in the items reactive variable.

The solution to this problem is called Props (properties). Theyre like HTML attributes, if they could contain anything! (arrays, objects, and even functions!)

So lets make a little change to ThoughtList.vue:

<template>    <ul>        <li v-for="item in items">            <ThoughtItem :message="item" />        </li>    </ul></template>

Hey! Its important to add the colon : before the prop name. It tells Vue to treat the content between "..." as JavaScript code (the item variable), instead of a String (the text "item").

And now, we can access the text message in ThoughtItem effortlessly:

<script setup>defineProps({  message: String, // `message` has type String.});</script><template>  <p>    {{ message }}  </p></template>

Hey! Just like defineEmits, defineProps is a compiler macro and doesnt require importing.

Loading data from the server

Up until now, we only used mock data. So, lets change that! Well be getting data from an external API.

But first, notice that well need to access our API from two separate components: ThoughtList for retrieving all items, and NewModal for adding new items to the list.

This is called a shared-state between multiple components. Theres no immediately obvious way of solving this issue, but thankfully, theres an entire library designed just for that, and its called Pinia.

This is where well start working with the stores/ folder. So lets create the src/stores/thoughts.js file.

Pinia store diagram

A store should always resemble this boilerplate, so use it as a starting-point:

// thoughts.jsimport { defineStore } from 'pinia'export default defineStore('thoughts', {    state: () => ({        // Reactive variables here    }),    actions: {        // Methods here    }})

We created a store with the "thoughts" ID name.

But what is a store, you may ask? Its simply a collection of reactive variables and methods that act on them:

// thoughts.jsimport { defineStore } from 'pinia'export default defineStore('thoughts', {    state() {        return {            reactive_var: 1,            another_reactive_var: 'awesome!',            again_another_reactive_var: [0, 2, 4]        }    },    actions: {        my_method() {            // We can access all reactive variables here using `this.`            // NOTE: we don't have to use `.value` here!            this.reactive_var++            return this.another_reactive_var * 4.5        }    }})

Notice how we dont have to use .value here. Awesome!

Now, lets create an actually useful store:

// thoughts.jsimport { defineStore } from "pinia";export default defineStore("thoughts", {  state() {    return {      items: [],    };  },  actions: {    async load() {      // Fetch data from the Cyclic API      const res = await fetch("https://ithink-api.cyclic.app/", {        headers: {          "Content-Type": "application/json",        },      });      const items = await res.json();      this.items = items;    },    async add(message) {      // Post data to the Cyclic API      await fetch("https://ithink-api.cyclic.app/", {        method: "post",        headers: {          "Content-Type": "application/json",        },        body: JSON.stringify({          text: message,        }),      });      this.items.unshift(message);    },  },});

All we did here is declare an items reactive variable that will contain our most important data. We then defined the load method that should be called to load data from the server and assign it to items.

We also defined the add action, which asynchronously sends data to the server and adds it to the items list.

Using real data in our application

Lets connect our thoughts store with our application! Well start with NewModal:

<script setup>    import getThoughtsStore from '../stores/thoughts.js'    const { add: addItem } = getThoughtsStore()    async function submit() {        await addItem(message.value)        close()    }</script>

We extracted the add function from the store by calling it as a function. Here, we called it getThoughtsStore, which is a convention when using Pinia.

Would you believe me if I told you that was it? Well, I never lie.

Lets move into ThoughtList, which will load data from the server in order to display it.

<script setup>import getThoughtsStore from "../stores/thoughts";const { load: loadItems } = getThoughtsStore();await loadItems();</script>

And we also have to access the items reactive variable straight from the store. So lets follow the same pattern here:

const { items } = getThoughtsStore(); // this is WRONG

But counter-intuitively, this breaks the reactivity of the variable. Heres the right way of doing this:

import { storeToRefs } from "pinia";const { items } = storeToRefs(getThoughtsStore()); // CORRECT :)

Perfect!

Notice how in our previous code example, were using a top-level await to load the data in ThoughtList:

<script setup>import getThoughtsStore from "@/stores/thoughts";const { load: loadItems } = getThoughtsStore();await loadItems();</script>

Components with top-level awaits are called async components.

If we leave it like this, were gonna get the following error: (check your devtools console)

Component <Anonymous>: setup function returned a promise, but no <Suspense> boundary was found in the parent component tree. A component with async setup() must be nested in a <Suspense> in order to be rendered.

Its telling us that we should use a component called <Suspense> as a parent to the async component. [<Suspense>](https://vuejs.org/guide/built-ins/suspense.html) is a built-in Vue component, so we can use it anywhere in our app. Lets use it in App.vue:

<script setup>import ThoughtList from "./components/ThoughtList.vue";</script><template>  <main>    <Suspense>      <ThoughtList />    </Suspense>  </main></template>

It now works perfectly! Yay!

Adding a loading state to our list

Currently, our ThoughtList component will be invisible until data is loaded. Thats inconvenient, and makes a great example of bad UX.

Thankfully, since were already using <Suspense>, we can directly tell it to show something else while its child is loading. Add this to App.vue:

<Suspense>    <ThoughtList />    <template #fallback>        <p>Loading...</p>    </template></Suspense>

As you can see, anything within <template #fallback> will be shown when ThoughtList is loading. Awesome!

But we can get even awesome-r than that. Lets show a loading skeleton!

<Suspense>    <ThoughtList />    <template #fallback>        <div class="flex flex-wrap gap-2">            <div v-for="i in 15" class="h-16 w-48 animate-pulse rounded bg-stone-50/10"></div>        </div>    </template></Suspense>

You should know that i in 15 is a shortcut that Vue offers us to loop over the range [1, , 15]. Super!

And with that done, our app is now complete! Now that wasnt so hard, was it?

Conclusion

We covered a handy amount of Vue features:

Some of these are common to all web frameworks, while some arent. Well compare Vue with the rest of its peers at the end of this series, so I suggest you keep on reading! Trust me, theres a whole lot more to learn!


Original Link: https://dev.to/eludadev/i-made-the-same-app-3-times-in-3-different-frameworks-heres-what-i-learned-5c7b

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To