Reactivity

Enables the automatic tracking of state changes and the updating of the user interface when data changes, allowing for dynamic responses

ref:

const var = ref("hello!");

  • Short for references
  • Define reactive variables so that when the value of a ref changes, any component or template that uses it will automatically update without requiring a page refresh
  • In templating, you can invoke these with double curly brackets {{ var }} to indicate that you want to rerender that element every time there is a change
  • Do not reassign or manipulate refs directly, do this in functions
  • In templating, you can directly invoke var, but in scripting, reach into var.value to unpack

computed:

const displayVar = computed(() => var.value);

  • Reactive getters that take in a function (i.e. JS anonymous function)
  • Only updates/recalculates every time the reactive variable it references changes
  • Allow you to think about efficient recomputations -> only recompute when you have to!
  • Because of this, we typically recommend it over regular JS functions depending on the functionality you are trying to achieve
const count = ref(0);
const doubled = computed(() => count.value * 2);

In this example, when count becomes 1, doubled will become 2.

watch:

  • computed properties do not inherently keep track of their previous values, only the current value
  • To track the previous value of a computed property, use a combination of a ref and a watch function
const count = ref(0);
const prevCount = ref(-1);

const doubled = computed(() => count.value * 2);
watch(doubled, (doubled, oldVal) => {
  prevCount.value = oldVal;
});

In this example, we can call watch to track the previous and current values of the computed property. The previous value gets stored in oldVal and updates prevCount.

Options: State

Define functionality and behavior of the component.

Props

  • Enable a parent to pass data to the child component
  • Can think of this as a v-bind where the child component reads a specific value from the parent component
  • defineProps will allow you to create a list of properties to indicate what to care about on what you are passing down from the parent
    • To use an prop value, use props.propName
const props = defineProps(["name", "age"]);
const currentName = ref(props.name | "");

This will initialize currentName to the name passed down from the parent component.

Emit

  • Enables a child to pass data to the parent component
  • defineEmits will allow you to create custom events by name so that every time you conduct the event, it will pass data with it
  • The data that you end up emitting is known as your event payload
  • e.g. PostListComponent listens for an emit event from the child CreatePostComponent

diagram 1

Store

  • A global state holding data that is shared among multiple components
  • Components that need it retrieve it from the store
    • e.g. logged in user information since user information will be needed across many components
  • If data is only used by one component, it should remain in the component
  • Can also be useful to pass information between components that do not have a parent-child relationship
  • One store = one file in the stores directory. (ex. toastStore and userStore)
  • Examples of other possible things to put into store:
    • Number of posts a user has left (can be saved in the userStore)
      • Add const userPostsRemaining= ref(0); → fetch the amount left using your routes → return {userStore...userPostsRemaining}
    • Location inputted by user
    • Shopping cart items
  • Code example: see toast.ts

Built-in Directives

Special HTML attributes with the prefix v- to allow the library to do dynamic changes to a DOM element.

v-bind (:)

  • Unidirectional/one way data bind of variables in Javascript to HTML
  • Enables data to be passed from parent to child, and thus any changes made in the parent are reflected in the child
  • Changes in the child cannot be reflected in the parent if v-bind is used since changes are not propagated back

v-on (@)

  • Used to attach event listeners to DOM elements and only changes data within child component
  • Handles events such as click, input, change, etc.
  • Every time there is a change, it will reevaluate the in-line callback function and update the reactive variable & any references to it

v-model (::)

  • Bi-directional/two-way data bind.
  • Can be used to:
    1. Bind component to component (similar to props, but binds two way instead of one way)
    2. Bind a reactive variable (i.e. refs) to an input element (e.g. a textbox)
  • Any changes will be propagated to all bindings
<script>
  const inputText = ref("This is my input text.");
</script>

<template>
  <input type="text" v-model="inputText" />
</template>

This is an example of use case 2 that creates a two-way bind of the reactive variable input to the input text that is displayed/can be entered by the user on the interface. If the user changes the text box, input will update and if input is ever altered, the text box will reflect those changes as well.

  • v-model can be functionally implemented with both v-bind and v-on

The earlier example is equivalent to:

<script>
  const inputText = ref("This is my input text.");
</script>

<template>
  <input
    v-bind:value="inputText"
    v-on:input="event => inputText = event.target.value"
  />
</template>

v-bind binds the value to the box while v-on listens for input events to update the input. event.target refers to the element that triggered the event, in this case changes to the input field. Then, value holds the current value entered by the user to update inputText.

v-if, v-else

  • Like the if-else logic we are familiar with, this will conditionally render a block if the directive’s expression evaluates to true
<button v-on:click="clicked = !clicked">Click me!</button>

<h1 v-if="clicked">Clicked</h1>
<h1 v-else>Unclicked</h1>

In this example, if we assume clicked is originally initialized to false, pressing the button will change the value of clicked to True, which will then render the Clicked header. Vice versa for pressing the button again to Unclicked.

v-for

  • Used to render a list of items based on an array
<script>
  const items = ref([{ message: "hello" }, { message: "goodbye" }]);
</script>

<template>
  <li v-for="item in items"></li>
</template>

This will allow you to render each of the items with the general functionality of a for loop. In our class, you may use this directive to display and put actions on posts or comments.
diagram 2

Single-File Component

Component

  • Generally corresponds to a section of the UI
  • Allows you to split the UI into independent pieces, where each section/piece can correspond to a modular functionality.
  • Example
    • CreatePostComponent corresponds to the UI section that enables a user to create new content and post it
    • PostComponent corresponds to one Post that exists on app, and supports all actions/functionality a user would expect on a single post e.g. edit/delete, upvote/like, etc.

Parent vs child component

  • A child component exists as an import in the parent component
  • If you have to import a component to use it for full functionality to be achieved, then the importing component is the parent, and the imported component is the child
  • The parent child component relationship enables you to abstract and encapsulate functionality that tends to get repeated or that easily turns a single component into a monolith structure
  • Example:
  • PostListComponent imports PostComponent (and others) and is thus the parent, while PostComponent is the child contained inside the parent
  • This is because PostListComponent functionality includes among others Listing all posts in the app, or posts filtered by the author name.
  • For the posts list to appear on the page, it needs to enumerate each post and the full functionality of the post. But each post is a modular piece/section with independent functionality provided by the PostComponent. The PostListComponent thus essentially calls on the PostComponent to enable the modular functionality for each post it is enumerating, making use of the ‘‘v-for’ built-in directive.
<article v-for="post in posts" :key="post._id">
  <PostComponent
    v-if="editing !== post._id"
    :post="post"
    @refreshPosts="getPosts"
    @editPost="updateEditing"
  />
  <EditPostForm
    v-else
    :post="post"
    @refreshPosts="getPosts"
    @editPost="updateEditing"
  />
</article>

In the example above, using the frontend starter code for guidance, imagine what the PostListComponent file would look like if you did not abstract the functionality of each independent section of the UI comprising of its children components.

Slots:

  • Pass template fragments from parent to child. Similar to passing a prop but takes in vue code instead of a primitive type. Helps with component reusability (ex, can render a form with different questions depending on the user’s previous answers or profile)
  • Can have named slots (useful if using more than one slot in a child component), conditional slots, default slot values, etc (see reference)
  • Example: We have a FancyButton component with this template code:

    <button class="fancy-btn">
      <slot></slot>
      <!-- slot outlet -->
    </button>
    

    The parent of FancyButton can then do:

    <FancyButton>
      Click me!
      <!-- slot content -->
    </FancyButton>
    

    To render:

    <button class="fancy-btn">Click me!</button>
    

    We can also pass in other valid Vue code that is not just a string:

    <FancyButton>
      <span style="color:red">Click me!</span>
      <AwesomeIcon name="plus" />
    </FancyButton>
    

slots diagram

Frontend Vue + Backend Routes:

How can we link our frontend to the backend?

  • Inside of <script> we define all JavaScript functionality
  • We define the asynchronous function calls to our backend
  • When we detect changes in the frontend, we can call the corresponding backend function
  • In our class, we keep all of our backend code in server in the directory
  • api/index.ts connects your api routes from your backend code (you don’t need to change this–we’ve already done this for you) but take a look if you want to understand better the connection
  • fetchy() is used as a wrapper function for fetching queries from your backend API endpoints in your frontend components
    • See starter code: each route has /api/...
  • Do not confuse this with your frontend routes! See Vue Router below
    • Frontend routes are housed in /client/router/index.ts and indicate how a user should navigate your app (the routes that allow them to access different views/pages)
async function getPosts(author?: string) {
  let query: Record<string, string> = author !== undefined ? { author } : {};
  let postResults;
  try {
    postResults = await fetchy("/api/posts", "GET", { query });
  } catch (_) {
    return;
  }
  searchAuthor.value = author ? author : "";
  posts.value = postResults;
}

The code shows getPosts which is called before a page loads using onBeforeMount and when posts are searched, created, deleted, and modified.

Vue Router

  • Our router: https://router.vuejs.org/guide/
  • Enables you to create different routes on the clients’ side to direct them to different pages
  • Different but similar in functionality to router on the server side for API calls
  • Enables you to create different routes and service each through different components on a single page application i.e. with a single html page
  • Each different path in the router renders a different component
  • See /client/router/index.ts in starter code

Dynamic routing

  • Create routes with parameters in Vue, making your application dynamic to different URLs allowing us to create similar pages that have different data (i.e. different user profiles)
  • Add routes in /client/App.vue
<RouterLink :to="{ name: 'Home' }" :class="{ underline: currentRouteName == 'Home' }">
  Home
</RouterLink>
  • router.addRoute() and router.removeRoute()
    • Register a new route so that if the newly added route matches the current location, you need to manually navigate by adding parameters to the route with router.push() or router.replace()
const routes = [{ path: '/user/:id', // :id is a dynamic value in the route
                   name: 'UserProfile',
                   component: UserProfile }];

// navigate to the route with a parameter
router.push({ name: 'UserProfile', params: { id: 123 } }); |
async function login() {
  await loginUser(username.value, password.value);
  await updateSession();
  void router.push({ name: "Home" });
}

Reroutes user to the Home page once they are logged in.

How to Structure a Component.vue:

Script:

  • <script></script>
  • JavaScript logic for component
  • Make all your imports here
  • Where you are storing your data or making calculations
  • Includes function calls to backend

Template:

  • <template></template>
  • Where HTML goes and what you want to display on the page

Style:

  • CSS
  • <style scoped></style>
    • Generally good to keep your css styling scoped so that it’s particular to the specific component and does not leak to other components (and vice versa)

Once you’ve created your component, you can use Vue components as HTML objects! i.e. LoginPage.vue

<script setup lang="ts">
  import LoginForm from "@/components/Login/LoginForm.vue";
  import RegisterForm from "@/components/Login/RegisterForm.vue";
</script>
<template>
  <main class="column">
    <h1>Please login or register!</h1>
    <LoginForm /> // using Component as HTML object <RegisterForm /> // using
    Component as HTML object
  </main>
</template>

Together they complete your page in a modular fashion!

FAQ

  • When should we use our backend database (MongoDB) vs a store?
    • Backend:
      1. Permanent data that needs to persist across multiple sessions OR
      2. Data that needs to be accessible by multiple different users
        • Examples: username/password, permanent user preferences
    • Store:
      1. Temporary data that needs to be persisted for the session, but not afterwards (i.e. data is relevant to the current user only) OR
      2. Reactive data that needs to be updated when the UI updates or that causes the UI to update
        • Examples: zip code for weather app, temporary guest profile avatars, temporary user preferences that get reset the next time they come back to the app
  • What should be in a component state vs a store?
    • Component state = data used only in that component or passed down as a prop
    • Store = data used across multiple components that may not have a parent-child relationship
  • At what point should one consider switching from passing props down to using a store?
    • When it starts getting hard to debug and/or understand where the original variable value is coming from. Since props are a one-way bind, if a deeply nested child component wants to update the value of the variable, you will need to send as many emit messages as the levels of the nesting. Using a store instead means you can just call updateVariable() to change the value.
    • Additionally, use a store when not every child component needs the same value. For example, let’s say you have components A, B, C where A is the parent of B which is the parent of C and you want C to read a prop value that is defined as a ref in A. Then, you have to first pass the variable to B even if B does not need the value. Using a store instead will allow C to access the variable without needing to go through B, making your code neater overall.
  • When should we use computed properties vs functions?
    • Computed properties are cached so that the computed ref’s value is not recalculated unless a dependency changes even when the component is rerendered. In contrast, functions are always run when a component is rerendered. Although the variables may have the same value at the end of the day, if the computation to find that variable is very expensive, using a function will have worse performance than a computer property.
    • The dependencies of a computed property are any reactive variables that are used in the calculations.
  • How do we connect the frontend with the backend in Vue?
    • See section Frontend Vue + Backend Routes
  • What does ref(…) return and when should we use them over regular variables?
    • Ref(…) returns a reactive variable object whose values you can access using refName.value or with double curly braces {{ refName }} . Reactive variables cause the UI to automatically update when their values change. With a regular variable, the user will have to refresh the page or your code will have to force a page refresh in order for the new value to show up in the UI.
    • You can explicitly add typing to ref objects to make them return the correctly typed values:
      • const year = ref<string | number>('2020')
      • import type { Ref } from "vue";
        import { ref } from "vue";
        const year: Ref<string | number> = ref("2020");
        

More resources