Storing data in a central location is crucial for complex front-end apps. Otherwise, data is spread everywhere and has to be passed between components.
This is especially important if you use frameworks like React, Angular, or Vue.js. These three frameworks divide apps into components and you have to pass data between them if you want to share data in an ad-hoc fashion.
React only has one-way binding between components, so you can only pass data down the component tree. In any case, if you have lots of data to pass around, it gets confusing very quickly.
In this piece, we will write a little game where the app gets your current location and shows you cat pictures. The app will ask you questions and you can go back and forth by click Yes and No buttons.
The data store will track which page you’re at so that you can move forward and backward.
We will use Vuex for storing the central state of our application. It is the most popular state management library for Vue.js apps with support from the Vue.js developers themselves.
To start, we need to install the Vue CLI to make building our app easier. It contains a development server and scripts to generate boilerplate code.
Run npm install -g @vue/cli
to install it globally.
Then, we run vue create geokittyjs
to scaffold our app.
After that, we can run the development server to display our app by running npm run serve
. It will refresh the browser automatically as we are updating our code.
We install our supporting libraries by running npm i yuex vue-material vue-router superagent
.
Now we are ready to code.
First, we add Material icons to index.html
to get pretty icons, like so:
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:400,500,700,400italic|Material+Icons">
Then, we write the logic part of the app. We start with some essential constants. We put them all in src/constants.js
for easy access.
In there, add:
let IPURL = 'https://api.ipify.org?format=json';
let LATLONGURL = 'https://api.ipgeolocation.io/ipgeo';
let IPKEY = 'get key from https://ipgeolocation.io/'
let MASHAPEKEY = 'get key from Mashape'
let GOOGLEAPIKEY = 'get key from Google';
let CONTINENTS = {
"AD": "Europe",
"AE": "Asia",
"AF": "Asia",
"AG": "North America",
"AI": "North America",
"AL": "Europe",
"AM": "Asia",
"AN": "North America",
"AO": "Africa",
"AQ": "Antarctica",
"AR": "South America",
"AS": "Australia",
"AT": "Europe",
"AU": "Australia",
"AW": "North America",
"AZ": "Asia",
"BA": "Europe",
"BB": "North America",
"BD": "Asia",
"BE": "Europe",
"BF": "Africa",
"BG": "Europe",
"BH": "Asia",
"BI": "Africa",
"BJ": "Africa",
"BM": "North America",
"BN": "Asia",
"BO": "South America",
"BR": "South America",
"BS": "North America",
"BT": "Asia",
"BW": "Africa",
"BY": "Europe",
"BZ": "North America",
"CA": "North America",
"CC": "Asia",
"CD": "Africa",
"CF": "Africa",
"CG": "Africa",
"CH": "Europe",
"CI": "Africa",
"CK": "Australia",
"CL": "South America",
"CM": "Africa",
"CN": "Asia",
"CO": "South America",
"CR": "North America",
"CU": "North America",
"CV": "Africa",
"CX": "Asia",
"CY": "Asia",
"CZ": "Europe",
"DE": "Europe",
"DJ": "Africa",
"DK": "Europe",
"DM": "North America",
"DO": "North America",
"DZ": "Africa",
"EC": "South America",
"EE": "Europe",
"EG": "Africa",
"EH": "Africa",
"ER": "Africa",
"ES": "Europe",
"ET": "Africa",
"FI": "Europe",
"FJ": "Australia",
"FK": "South America",
"FM": "Australia",
"FO": "Europe",
"FR": "Europe",
"GA": "Africa",
"GB": "Europe",
"GD": "North America",
"GE": "Asia",
"GF": "South America",
"GG": "Europe",
"GH": "Africa",
"GI": "Europe",
"GL": "North America",
"GM": "Africa",
"GN": "Africa",
"GP": "North America",
"GQ": "Africa",
"GR": "Europe",
"GS": "Antarctica",
"GT": "North America",
"GU": "Australia",
"GW": "Africa",
"GY": "South America",
"HK": "Asia",
"HN": "North America",
"HR": "Europe",
"HT": "North America",
"HU": "Europe",
"ID": "Asia",
"IE": "Europe",
"IL": "Asia",
"IM": "Europe",
"IN": "Asia",
"IO": "Asia",
"IQ": "Asia",
"IR": "Asia",
"IS": "Europe",
"IT": "Europe",
"JE": "Europe",
"JM": "North America",
"JO": "Asia",
"JP": "Asia",
"KE": "Africa",
"KG": "Asia",
"KH": "Asia",
"KI": "Australia",
"KM": "Africa",
"KN": "North America",
"KP": "Asia",
"KR": "Asia",
"KW": "Asia",
"KY": "North America",
"KZ": "Asia",
"LA": "Asia",
"LB": "Asia",
"LC": "North America",
"LI": "Europe",
"LK": "Asia",
"LR": "Africa",
"LS": "Africa",
"LT": "Europe",
"LU": "Europe",
"LV": "Europe",
"LY": "Africa",
"MA": "Africa",
"MC": "Europe",
"MD": "Europe",
"ME": "Europe",
"MG": "Africa",
"MH": "Australia",
"MK": "Europe",
"ML": "Africa",
"MM": "Asia",
"MN": "Asia",
"MO": "Asia",
"MP": "Australia",
"MQ": "North America",
"MR": "Africa",
"MS": "North America",
"MT": "Europe",
"MU": "Africa",
"MV": "Asia",
"MW": "Africa",
"MX": "North America",
"MY": "Asia",
"MZ": "Africa",
"NA": "Africa",
"NC": "Australia",
"NE": "Africa",
"NF": "Australia",
"NG": "Africa",
"NI": "North America",
"NL": "Europe",
"NO": "Europe",
"NP": "Asia",
"NR": "Australia",
"NU": "Australia",
"NZ": "Australia",
"OM": "Asia",
"PA": "North America",
"PE": "South America",
"PF": "Australia",
"PG": "Australia",
"PH": "Asia",
"PK": "Asia",
"PL": "Europe",
"PM": "North America",
"PN": "Australia",
"PR": "North America",
"PS": "Asia",
"PT": "Europe",
"PW": "Australia",
"PY": "South America",
"QA": "Asia",
"RE": "Africa",
"RO": "Europe",
"RS": "Europe",
"RU": "Europe",
"RW": "Africa",
"SA": "Asia",
"SB": "Australia",
"SC": "Africa",
"SD": "Africa",
"SE": "Europe",
"SG": "Asia",
"SH": "Africa",
"SI": "Europe",
"SJ": "Europe",
"SK": "Europe",
"SL": "Africa",
"SM": "Europe",
"SN": "Africa",
"SO": "Africa",
"SR": "South America",
"ST": "Africa",
"SV": "North America",
"SY": "Asia",
"SZ": "Africa",
"TC": "North America",
"TD": "Africa",
"TF": "Antarctica",
"TG": "Africa",
"TH": "Asia",
"TJ": "Asia",
"TK": "Australia",
"TM": "Asia",
"TN": "Africa",
"TO": "Australia",
"TR": "Asia",
"TT": "North America",
"TV": "Australia",
"TW": "Asia",
"TZ": "Africa",
"UA": "Europe",
"UG": "Africa",
"US": "North America",
"UY": "South America",
"UZ": "Asia",
"VC": "North America",
"VE": "South America",
"VG": "North America",
"VI": "North America",
"VN": "Asia",
"VU": "Australia",
"WF": "Australia",
"WS": "Australia",
"YE": "Asia",
"YT": "Africa",
"ZA": "Africa",
"ZM": "Africa",
"ZW": "Africa"
}
export { IPURL, LATLONGURL, MASHAPEKEY, GOOGLEAPIKEY, CONTINENTS, IPKEY };
src.constants.js
We need them for our API calls later.
Then, we create the data store for storing our data in src/store/store.js
:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
},
reset(state) {
state.count = 0;
},
disappoint(state) {
state.count = 9;
}
},
getters: {
getCount: state => {
return state.count
}
}
})
export default store;
Store.js
This will store the stage of the app that we are in so we know which page to go to. The code above is where the state management magic happens.
Now, we can create our components. We’ll create the pages that you will see.
In src/components
, add a file called City.vue
and put in the following code:
<template>
<div class="hello">
<md-content>
<h1>Can your choice in kittens reveal where you live?</h1>
<h1>You live in {{city}}.</h1>
<md-button class="md-raised md-accent" @click="next()">Yes</md-button>
<md-button class="md-raised md-accent" @click="disappoint()">No</md-button>
</md-content>
</div>
</template>
<script>
import store from "../store/store";
export default {
name: "City",
data() {
return {};
},
props: ["city"],
methods: {
next() {
store.commit("increment");
},
disappoint() {
store.commit("disappoint");
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.md-card {
width: 48vw;
margin: 4px;
vertical-align: top;
}
</style>
City.vue
Then, create a file called Continent.vue
and add:
<template>
<div class="hello">
<md-content>
<h1>Can your choice in kittens reveal where you live?</h1>
<h1>Hmm, your first choice suggests you live in {{continent}}. Is that correct?
</h1>
<md-button class="md-raised md-accent" @click="next()">Yes</md-button>
<md-button class="md-raised md-accent" @click="disappoint()">No</md-button>
</md-content>
</div>
</template>
<script>
import store from "../store/store";
export default {
name: "Continent",
data() {
return {};
},
props: ["continent"],
methods: {
next() {
store.commit("increment");
},
disappoint() {
store.commit("disappoint");
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.md-card {
width: 48vw;
margin: 4px;
vertical-align: top;
}
</style>
Continent.vue
Next, create a file called Country.vue
and add:
<template>
<div class="hello">
<md-content>
<h1>Can your choice in kittens reveal where you live?</h1>
<h1>We're thinking {{country}}. Is that right?</h1>
<md-button class="md-raised md-accent" @click="next()">Yes</md-button>
<md-button class="md-raised md-accent" @click="disappoint()">No</md-button>
</md-content>
</div>
</template>
<script>
import store from "../store/store";
export default {
name: "Country",
data() {
return {};
},
props: ["country"],
methods: {
next() {
store.commit("increment");
},
disappoint() {
store.commit("disappoint");
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.md-card {
width: 48vw;
margin: 4px;
vertical-align: top;
}
</style>
Country.vue
Create a file called Disappoint.vue
and add:
<template>
<div class="hello">
<md-content>
<h1>Rawr! Sry 2 Disapoint.</h1>
<md-button class="md-raised md-accent" @click="reset()">Try Again</md-button>
</md-content>
</div>
</template>
<script>
import store from "../store/store";
export default {
name: "Home",
data() {
return {};
},
methods: {
reset() {
store.commit("reset");
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.md-card {
width: 48vw;
margin: 4px;
vertical-align: top;
}
</style>
Disappoint.vue
Then, create a file called Home.vue
and:
<template>
<div class="hello">
<md-toolbar class="md-accent">
<h3 class="md-title">GeoKitty</h3>
</md-toolbar>
<div v-if="continent && country && region">
<where-continent v-if="count == 0"></where-continent>
<continent v-if="count == 1" :continent="continent"></continent>
<where-country v-if="count == 2"></where-country>
<country v-if="count == 3" :country="country"></country>
<where-region v-if="count == 4"></where-region>
<region v-if="count == 5" :region="region"></region>
<where-city v-if="count == 6"></where-city>
<city v-if="count == 7" :city="city"></city>
<win v-if="count == 8"></win>
<disappoint v-if="count == 9"></disappoint>
</div>
<div v-else>
<h1>Loading...</h1>
</div>
</div>
</template>
<script>
import store from "../store/store";
import {
IPURL,
LATLONGURL,
MASHAPEKEY,
GOOGLEAPIKEY,
CONTINENTS
} from "../constants";
const request = require("superagent");
export default {
name: "Home",
data() {
return {
continent: "",
country: "",
region: "",
city: ""
};
},
computed: {
count() {
return store.state.count;
}
},
methods: {
getLocation() {
request
.get(IPURL)
.then(res => {
let ip = res.body.ip;
return request
.get(`${LATLONGURL}?apiKey=${IPKEY}&ip=${ip}`)
.set("Content-Type", "application/json");
})
.then(res => {
let result = JSON.parse(res.text);
let lat = result.latitude;
let long = result.longitude;
this.country = result.country_name;
this.region = result.state_prov;
return request
.get(`https://maps.googleapis.com/maps/api/geocode/json`)
.query({ latlng: `${lat},${long}`, key: GOOGLEAPIKEY });
})
.then(res => {
this.continent =
CONTINENTS[res.body.results[0].address_components[6].short_name];
this.city = res.body.results[0].address_components[3].long_name;
})
.catch(err => {});
}
},
beforeMount() {
this.getLocation();
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.md-accent {
background-color: #ff5252;
background-color: var(--md-theme-demo-light-accent, #ff5252);
color: white;
}
.md-card {
width: 48vw;
margin: 4px;
vertical-align: top;
}
</style>
Home.vue
The logic of the code is as follows — we get your public IP address from the first API call, then, from that, it can get your location data.
In the template, we check for the state which is returned in the logic below, in the computed
property:
count() {
return store.state.count;
}
Whenever the state is updated, the count
function will return the latest value. Whatever is updated automatically, you can put it in the computed
property.
In Region.vue
, which we create, add:
<template>
<div class="hello">
<md-content>
<h1>Can your choice in kittens reveal where you live?</h1>
<h1>{{region}}? Is that right?</h1>
<md-button class="md-raised md-accent" @click="next()">Yes</md-button>
<md-button class="md-raised md-accent" @click="disappoint()">No</md-button>
</md-content>
</div>
</template>
<script>
import store from "../store/store";
export default {
name: "Region",
props: ["region"],
data() {
return {};
},
methods: {
next() {
store.commit("increment");
},
disappoint() {
store.commit("disappoint");
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.md-card {
width: 48vw;
margin: 4px;
vertical-align: top;
}
</style>
Region.vue
The rest is created in a similar way.
We create WhereCity.vue
, WhereContinent.vue
, WhereCountry.vue
, WhereRegion.vue
, and Win.vue
.
In WhereCIty.vue
, we add:
<template>
<div class="hello">
<md-content>
<h1>Can your choice in kittens reveal where you live?</h1>
<h1>Finally, which of these two do you prefer?</h1>
<md-card class="left">
<md-card-media>
<img src='../assets/7.jpg' alt="cat" @click="next()">
</md-card-media>
</md-card>
<md-card class="right">
<md-card-media>
<img src='../assets/8.jpg' alt="cat" @click="next()">
</md-card-media>
</md-card>
</md-content>
</div>
</template>
<script>
import store from "../store/store";
export default {
name: "WhereCity",
data() {
return {};
},
methods: {
next() {
store.commit("increment");
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.md-card {
width: 48vw;
margin: 4px;
vertical-align: top;
}
</style>
****WhereCIty.vue
In WhereContinent.vue
, we add:
<template>
<div class="hello">
<md-content>
<h1>Can your choice in kittens reveal where you live?</h1>
<h1>Start by clicking your favorite kitten.</h1>
<md-card class="left">
<md-card-media>
<img src='../assets/1.jpg' alt="cat" @click="next()">
</md-card-media>
</md-card>
<md-card class="right" >
<md-card-media>
<img src='../assets/2.jpg' alt="cat" @click="next()">
</md-card-media>
</md-card>
</md-content>
</div>
</template>
<script>
import store from "../store/store";
export default {
name: "WhereContinent",
data() {
return {};
},
methods: {
next() {
store.commit("increment");
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.md-card {
width: 48vw;
margin: 4px;
vertical-align: top;
}
</style>
WhereContinent.vue
In WhereCountry.vue
, we add:
<template>
<div class="hello">
<md-content>
<h1>Can your choice in kittens reveal where you live?</h1>
<h1>Ok, now select between these cuties..</h1>
<md-card class="left">
<md-card-media>
<img src='../assets/3.jpg' alt="cat" @click="next()">
</md-card-media>
</md-card>
<md-card class="right">
<md-card-media>
<img src='../assets/4.jpg' alt="cat" @click="next()">
</md-card-media>
</md-card>
</md-content>
</div>
</template>
<script>
import store from "../store/store";
export default {
name: "WhereCountry",
data() {
return {};
},
methods: {
next() {
store.commit("increment");
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.md-card {
width: 48vw;
margin: 4px;
vertical-align: top;
}
</style>
WhereCountry.vue
In WhereRegion.vue
, we add:
<template>
<div class="hello">
<md-content>
<h1>Can your choice in kittens reveal where you live?</h1>
<h1>Your favorite between these two?</h1>
<md-card class="left">
<md-card-media>
<img src='../assets/5.jpg' alt="cat" @click="next()">
</md-card-media>
</md-card>
<md-card class="right">
<md-card-media>
<img src='../assets/6.jpg' alt="cat" @click="next()">
</md-card-media>
</md-card>
</md-content>
</div>
</template>
<script>
import store from "../store/store";
export default {
name: "WhereRegion",
data() {
return {};
},
methods: {
next() {
store.commit("increment");
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.md-card {
width: 48vw;
margin: 4px;
vertical-align: top;
}
</style>
WhereRegion.vue
And finally, in Win.vue
, we add:
<template>
<div class="hello">
<md-content>
<h1>Meow! Meez win again! </h1>
<md-button class="md-raised md-accent" @click="reset()">Try Again</md-button>
</md-content>
</div>
</template>
<script>
import store from "../store/store";
export default {
name: "Home",
data() {
return {};
},
methods: {
reset() {
store.commit("reset");
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.md-card {
width: 48vw;
margin: 4px;
vertical-align: top;
}
</style>
Win.vue
Note that, in each file, we have a call for store.commit
. This is where the state is updated.
In all files, if there are images referenced in the templates, you can add cat pictures with the same name to the folder referenced in the template.
Then, in src/router/index.js
, we register our components and libraries so that that we can use them in other parts of our apps, like so:
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import VueMaterial from 'vue-material'
import 'vue-material/dist/vue-material.min.css'
import WhereContinent from '@/components/WhereContinent'
import WhereCity from '@/components/WhereCity'
import WhereCountry from '@/components/WhereCountry'
import WhereRegion from '@/components/WhereRegion'
import City from '@/components/City'
import Continent from '@/components/Continent'
import Country from '@/components/Country'
import Region from '@/components/Region'
import Win from '@/components/Win'
import Disappoint from '@/components/Disappoint'
Vue.use(Router)
Vue.use(VueMaterial)
Vue.component('where-continent', WhereContinent)
Vue.component('where-city', WhereCity)
Vue.component('where-country', WhereCountry)
Vue.component('where-region', WhereRegion)
Vue.component('city', City)
Vue.component('continent', Continent)
Vue.component('country', Country)
Vue.component('region', Region)
Vue.component('win', Win)
Vue.component('disappoint', Disappoint)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
}
]
})
src.router.index.js
Finally, we get something like this:
☞ JavaScript Programming Tutorial Full Course for Beginners
☞ Learn JavaScript - Become a Zero to Hero
☞ E-Commerce JavaScript Tutorial - Shopping Cart from Scratch