How to Use Environment Variables in Your Vue.js App

Vue CLI makes using them in your app easy by allowing variables in an .env file that have keys starting with VUE_APP to be used in your app.

Environment variables are often used for things that do not belong in your code like API keys and URLs. Vue CLI makes using them in your app easy by allowing variables in an .env file that have keys starting with VUE_APP to be used in your app.

The variables can be accessed by using the process.env object. For example, if you have VUE_APP_API_KEY in your .env file, then you can access it by using process.env.VUE_APP_API_KEY .

You can also have an .env file for other environments by adding an extension to the .env file. For example, you can use .env.staging for the staging environment if you want to include your staging URLs when you want to deploy to your staging server.

In this article, we will build a photo app that allows users to search for images and display images in a masonry grid. The image grid will have infinite scroll to get more images. We will use the vue-masonry library for render the image grid, and vue-infinite-scroll for the infinite scrolling effect.

To implement the masonry effect, we have to set the width of the image proportional to the screen width and set the image height to be proportional to the aspect ratio of the image.

This is a pain to do if it’s done without any libraries, so people have made libraries to create this effect.

Our app will display images from the Pixabay API. You can view the API documentation and register for a key at https://pixabay.com/api/docs/

We will store the API key in the .env file on the project’s root folder.

Once we have the Pixabay API key, we can start writing our app. To start, we create a project called photo-app . To do this, run:

npx @vue/cli create photo-app

This will create the files for our app and install the packages for the built-in libraries. We choose ‘manually select features’ and choose Babel, Vue Router and CSS Preprocessor.

Next, we install our own packages. We need the vue-masonry library and vue-infinite-scroll we mentioned above. In addition, we need BootstrapVue for styling, Axios for making HTTP requests and Vee-Validate for form validation.

We install all the packages by running:

npm i axios bootstrap-vue vee-validate vue-infinite-scroll vue-masonry

With all the packages installed, we can start writing our app. Create a mixins folder in the src folder and create a requestsMixin.js file.

Then we add the following to the file:

const axios = require("axios");
const APIURL = "https://pixabay.com/api";

export const requestsMixin = {
  methods: {
    getImages(page = 1) {
      return axios.get(`${APIURL}/?page=${page}&key=${process.env.VUE_APP_API_KEY}`);
    },

    searchImages(keyword, page = 1) {
      return axios.get(
        `${APIURL}/?page=${page}&key=${process.env.VUE_APP_API_KEY}&q=${keyword}`
      );
    }
  }
};

We call the endpoints to search for images here. process.env.VUE_APP_API_KEY is retrieved from the .env file in the root folder of our project. Note that the environment variables we use have to have keys that begin with VUE_APP .

Next, in Home.vue , replace the existing code with:

<template>
  <div class="page">
    <h1 class="text-center">Home</h1>
    <div
      v-infinite-scroll="getImagesByPage"
      infinite-scroll-disabled="busy"
      infinite-scroll-distance="10"
    >
      <div
        v-masonry="containerId"
        transition-duration="0.3s"
        item-selector=".item"
        gutter="5"
        fit-width="true"
        class="masonry-container"
      >
        <div>
          <img
            :src="item.previewURL"
            v-masonry-tile
            class="item"
            v-for="(item, index) in images"
            :key="index"
          />
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { requestsMixin } from "../mixins/requestsMixin";

export default {
  name: "home",
  mixins: [requestsMixin],
  data() {
    return {
      images: [],
      page: 1,
      containerId: null
    };
  },
  methods: {
    async getImagesByPage() {
      const response = await this.getImages(this.page);
      this.images = this.images.concat(response.data.hits);
      this.page++;
    }
  },
  beforeMount() {
    this.getImagesByPage();
  }
};
</script>

We use the vue-infinite-scroll and vue-masonry packages here. Note that we specified the transition-duration to tweak the transition from showing nothing to showing the images, fit-width makes the columns fit the container. gutter specifies the width of the space between each column in pixels. We also set a CSS class name in the v-masonry container to change the styles later.

Inside the v-masonry div, we loop through the images, we set the v-masonry-tile to indicate that it is tile so that it will resize them to a masonry grid.

In the script object, we get the images when the page loads with the beforeMount hook. Since we are adding infinite scrolling, we keep adding images to the array as the user scrolls down. We call getImagesByPage as the user scrolls down as indicated by the v-infinite-scroll prop. We set infinite-scroll-disabled to busy to set disable scrolling if busy is set to true . infinite-scroll-distance indicates the distance from the bottom of the page in percent for scrolling to be triggered.

Next create ImageSearchPage.vue in the views folder and add:

<template>
  <div class="page">
    <h1 class="text-center">Image Search</h1>
    <ValidationObserver ref="observer" v-slot="{ invalid }">
      <b-form @submit.prevent="onSubmit" novalidate>
        <b-form-group label="Keyword" label-for="keyword">
          <ValidationProvider name="keyword" rules="required" v-slot="{ errors }">
            <b-form-input
              :state="errors.length == 0"
              v-model="form.keyword"
              type="text"
              required
              placeholder="Keyword"
              name="keyword"
            ></b-form-input>
            <b-form-invalid-feedback :state="errors.length == 0">Keyword is required</b-form-invalid-feedback>
          </ValidationProvider>
        </b-form-group>

        <b-button type="submit" variant="primary">Search</b-button>
      </b-form>
    </ValidationObserver>

    <br />

    <div
      v-infinite-scroll="searchAllImages"
      infinite-scroll-disabled="busy"
      infinite-scroll-distance="10"
    >
      <div
        v-masonry="containerId"
        transition-duration="0.3s"
        item-selector=".item"
        gutter="5"
        fit-width="true"
        class="masonry-container"
      >
        <div>
          <img
            :src="item.previewURL"
            v-masonry-tile
            class="item"
            v-for="(item, index) in images"
            :key="index"
          />
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { requestsMixin } from "../mixins/requestsMixin";

export default {
  mixins: [requestsMixin],
  data() {
    return {
      form: {},
      page: 1,
      containerId: null,
      images: []
    };
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      this.page = 1;
      await this.searchAllImages();
    },

    async searchAllImages() {
      if (!this.form.keyword) {
        return;
      }
      const response = await this.searchImages(this.form.keyword, this.page);
      if (this.page == 1) {
        this.images = response.data.hits;
      } else {
        this.images = this.images.concat(response.data.hits);
      }
      this.page++;
    }
  }
};
</script>

The infinite scrolling and masonry layout are almost the same, except when the keyword changes, we reassign the this.images array to the new items instead of keep adding them to the existing array so that users see the new results.

The form is wrapped inside the ValidationObserver so that we can get the validation status of the whole form inside the ValidationObserver . In the form, we wrap the input with ValidationProvider so that the form field can be validated and a validation error message displayed for the input. We check if keyword is filled in.

Once the user clicks Search, onSubmit is run, which runs await this.$refs.observer.validate(); to get the form validation status. If that results to true , then searchAllImages will be run to get the images.

Next, we replace the existing code in App.vue with:

<template>
  <div>
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="#">Photo App</b-navbar-brand>

      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>

      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item to="/" :active="path == '/'">Home</b-nav-item>
          <b-nav-item to="/imagesearch" :active="path == '/imagesearch'">Image Search</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <router-view />
  </div>
</template>

<script>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>

<style lang="scss">
.page {
  padding: 20px;
}

.item {
  width: 30vw;
}

.masonry-container {
  margin: 0 auto;
}
</style>

We add the BootstrapVue b-navbar here to display a top bar with links to our pages. In the script section, we watch the current route by getting this.$route.path . We set the active prop by check the path against our watched path to highlight the links.

In the style section, we set the padding of our pages with the page class, we set the photo width with the item class as indicated in the item-selector of our v-masonry div, and we set the masonry-container ‘s margin to 0 auto so that it will be centered in the page.

Next in main.js , replace the existing code with:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import BootstrapVue from "bootstrap-vue";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
import { VueMasonryPlugin } from "vue-masonry";
import infiniteScroll from "vue-infinite-scroll";

Vue.config.productionTip = false;

extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(VueMasonryPlugin);
Vue.use(infiniteScroll);
Vue.use(BootstrapVue);

new Vue({
  router,
  render: h => h(App)
}).$mount("#app");

to add all the libraries we used in the components and the Vee-Validate validation rules that we used. Also, we import our Bootstrap styles here so that we see the styles everywhere.

Next in router.js , replace the existing code with:

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import ImageSearchPage from "./views/ImageSearchPage.vue";

Vue.use(Router);

export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    },
    {
      path: "/imagesearch",
      name: "imagesearch",
      component: ImageSearchPage
    }
  ]
});

to add our routes.

Finally, in index.html , we replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title>Photo App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-masonry-tutorial-app doesn't work properly without
        JavaScript enabled. Please enable it to continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

to rename the title of our app.