This page looks best with JavaScript enabled

Create Reddit Reader Using Vue 3 & Vite

 ·   ·  ☕ 7 min read

Hello everyone! Hope you are all set for the new year. While you are waiting for the Y2020 to end with bated breath, here’s a post to kick start your Vue 3 journey. We will create a Reddit reader using Vue3 and Vite!

This post is more useful for someone with basic knowledge of Vue and Vue 2.

Get Started: Installation

Create a Vue 3 project with Vite..

1
npm create @vitejs/app

Select the vue template on prompt to create a new folder for your project. Install dependencies.

1
2
cd reddit-reader-vite-vue-app
npm i

Run the project -

1
npm run dev

The project starts almost instantaneously.. sweet. Do two more things -

  • Open the project folder in VSCode
  • Open http://localhost:3000/ in your browser

Edit the file index.html and include reference to a light-weight CSS framework. I use chota css.

1
<link rel="stylesheet" href="https://unpkg.com/chota@latest" />

Create a blank file at src/assets/css/styles.css to create custom styles (we will include this file in main.js). I will not explicitly outline any custom styles here, but you should find them here on the Github repo.

Yay.. you have this beautiful app.

vite3-initial-page

You are all set! Let us start coding in the app.

Setup Vue Router

We will have a bunch of links (well, two to be exact) to navigate within the app. Vue Router makes that routing real easy. Let’s set it up.

First, install Vue router compatible with Vue3.

1
npm i vue-router@4

Next, create the router file src/router/index.js -

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { createWebHistory, createRouter } from "vue-router";
import Home from "../views/Home.vue";
const history = createWebHistory();

const routes = [
  { path: "/", component: Home },
  {
    path: "/top",
    component: () => import("../views/Posts.vue"),
    props: { filter: "top" },
  },
  {
    path: "/controversial",
    component: () => import("../views/Posts.vue"),
    props: { filter: "controversial" },
  },
];

const router = createRouter({ history, routes });
export default router;

We will use the same view Posts for both top and controversial posts. The view will decide which data to fetch based on the prop called filter, which gets passed by the router.

Change src/main.js to use the router file and include the custom style file we had created earlier -

1
2
3
4
5
6
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import "./assets/css/styles.css";

createApp(App).use(router).mount("#app");

There’s a small problem with the router code - we don’t have those views yet. Let’s do that now.

Setup Views

We will create two new views and setup App to use the router.

Home

Create a new view for the home page - src/views/Home.vue.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<template>
  <h3>Reddit Reader</h3>

  <p>This is a simple Reddit Reader app.</p>
  <div class="card hero justify-text">
    <p class="is-center text-light">Start Here!</p>
    <router-link to="/top">Top Posts</router-link>
    &nbsp; | &nbsp;
    <router-link to="/controversial">Controversial Posts</router-link>
  </div>
</template>

Posts

Create a new view for showing the list of posts - src/views/Posts.vue.

1
2
3
4
5
6
7
<template>
  <h4>Posts</h4>
</template>

<script setup></script>

<style></style>

App.vue

Change code in src/App.vue to make use of router.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
  <header></header>
  <router-view :key="$route.path" />
  <footer></footer>
</template>

<script setup>
  import Header from "./components/Header.vue";
  import Footer from "./components/Footer.vue";
</script>

<style>
  #app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    margin-top: 60px;
  }
</style>

Let’s also create two new components for header and footer, which display the logo/brand, navbar and footer for the entire site.

Create new file src/components/Header.vue -

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<template>
  <nav class="nav">
    <div class="nav-left">
      <a class="logo-brand" href="/">Reddit Reader</a>
    </div>
    <div class="nav-right">
      <div class="tabs">
        <router-link to="/top">Top</router-link>
        <router-link to="/controversial">Controversial</router-link>
      </div>
    </div>
  </nav>
</template>

Create new file src/components/Footer.vue -

1
2
3
4
5
<template>
  <footer class="text-grey bg-light">
    <div class="container is-left">i am groot</div>
  </footer>
</template>

Change Post View

Let’s go back to our posts view and do just one thing - outline a list of top and controversial posts. No pagination, showing the page details - nothing. We just take the user to Reddit when a link is clicked. We will use the all new Vue composition API and features.

Let’s start by extending the template created earlier to receive props from router, call Reddit API, and display the results.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<template>
  <h4>
    <strong class="text-light">posts</strong>
  </h4>

  <div class="container">
    <p class="error-msg">{{ err }}</p>
    {{ posts }}
  </div>
</template>

<script setup>
  import { onMounted, ref, defineProps } from "vue";

  const posts = ref([]);
  const err = ref("");

  const mounted = onMounted(async () => {
    console.log("Fetching them posts..", props.filter);
    posts.value = await fetchPosts();
    console.log("posts.value: ", posts.value);
  });

  const props = defineProps({ filter: { type: String, default: "top" } });

  const fetchPosts = async (query) => {
    try {
      err.value = "";
      const res = await fetch(
        `https://api.reddit.com/${props.filter}?limit=10`
      );
      if (res.ok) {
        const resJson = await res.json();
        return resJson?.data?.children;
      } else {
        err.value = res.statusText;
        return [];
      }
      console.log("data: ", data);
    } catch (e) {
      console.error(e.message);
      return [];
      err.value = e.message;
    }
  };
</script>

Straight off the bat you will notice a few curiosities.

Variables

There are no distinct sections that will be exported by default. Instead, we make do with a new function called setup().

1
2
3
4
5
<script>
  setup() {
    // ...
  }
</script>

This is further simplified by using <script setup> tag.

1
2
3
<script setup>
  import { onMounted, ref, defineProps } from "vue";
</script>
Variables and State

Instead of a data() section in Vue2, we directly define variables in script (you could always mix and match, and confuse yourself and others though).

But, we need to tell Vue that we are creating a state variable that may also be reactive. We do that using ref.

1
2
3
4
5
6
<script>
  // ...
  const posts = ref([]);
  const err = ref("");
  // ...
</script>
Props

Define props with -

1
2
3
4
5
<script>
  // ...
  const props = defineProps({ filter: { type: String, default: "top" } });
  // ...
</script>
Life Cycle Hooks

We use onMounted hook to fetch data from Reddit.

1
2
3
4
5
6
7
<script setup>
  const mounted = onMounted(async () => {
    console.log("Fetching them posts..", props.filter);
    posts.value = await fetchPosts();
    console.log("posts.value: ", posts.value);
  });
</script>
Methods

Methods can be defined anywhere within script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
  // ...
  const fetchPosts = async (query) => {
    try {
      err.value = "";
      const res = await fetch(
        `https://api.reddit.com/${props.filter}?limit=10`
      );
      if (res.ok) {
        const resJson = await res.json();
        return resJson?.data?.children;
      } else {
        err.value = res.statusText;
        return [];
      }
      console.log("data: ", data);
    } catch (e) {
      console.error(e.message);
      return [];
      err.value = e.message;
    }
  };
  // ...
</script>

With a few cosmetic chahges, the final code for Posts.vue is outlined below -

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<template>
  <h4>
    <strong class="text-light">{{ props.filter || "" }} posts</strong>
  </h4>
  <div class="container">
    <p class="error-msg">{{ err }}</p>
    <div v-if="!posts || posts.length <= 0">Loading..</div>
    <div
      class="post-item card text-left"
      v-for="(post, index) in posts"
      :key="index"
      v-else
    >
      <h3 class="post-title">
        <a :href="post?.data?.url" target="_blank">{{ post?.data?.title }}</a>
      </h3>
      <div class="post-item-meta">
        <a
          :href="`https://reddit.com/${post?.data?.subreddit_name_prefixed}`"
          target="blank"
          >{{ post?.data?.subreddit_name_prefixed }}</a
        >
        | {{ post?.data?.num_comments }} comments
      </div>
    </div>
  </div>
</template>

<script setup>
  import { onMounted, ref, defineProps } from "vue";

  const posts = ref([]);
  const err = ref("");

  const mounted = onMounted(async () => {
    console.log("Fetching them posts..", props.filter);
    posts.value = await fetchPosts();
    console.log("posts.value: ", posts.value);
  });

  const props = defineProps({ filter: { type: String, default: "top" } });

  const fetchPosts = async (query) => {
    try {
      err.value = "";
      const res = await fetch(
        `https://api.reddit.com/${props.filter}?limit=10`
      );
      if (res.ok) {
        const resJson = await res.json();
        return resJson?.data?.children;
      } else {
        err.value = res.statusText;
        return [];
      }
      console.log("data: ", data);
    } catch (e) {
      console.error(e.message);
      return [];
      err.value = e.message;
    }
  };
</script>

<style></style>

Your app is now ready to rock!

reddit-reader-posts-vite

Finis

You will find the complete code at this Github repo.

Stay in touch!
Share on

Prashanth Krishnamurthy
WRITTEN BY
Prashanth Krishnamurthy
Technologist | Creator of Things


What's on this Page