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..
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 -
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.
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.
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>
|
<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!
Finis
You will find the complete code at this Github repo.