As you build out Vue.js applications and they begin to reach a certain size, you will likely run into the need for global state management. Conveniently, the core development team provides Vuex, the de facto state management library for Vue.js applications.
Getting started is pretty simple, and I’m going to assume you already are familiar with implementing Vuex. This post is not about getting started after all. If you need that, then I would recommend checking out the documentation.
Vuex makes managing a global data store much simpler, and for the following examples, let’s assume we have a store that looks something like this:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
user: null
},
mutations: {
setUser (state, user) {
state.user = user
}
},
})
The store’s state begins with an empty user
object, and a setUser
mutation that can update the state. Then in our application we may want to show the user details:
<template>
<div>
<p v-if="user">Hi {{ user.name }}, welcome back!</p>
<p v-else>You should probably log in.</p>
</div>
</template>
<script>
export default {
computed {
user() {
return this.$store.state.user
}
}
}
</script>
So, when the App loads it shows the user a welcome message if they are logged in. Otherwise, it tells them they need to log in. I know this is a trivial example, but hopefully, you have run into something similar to this.
If you’re like me, the question comes up:
How do I add data to my store before my app loads?
Well, there are a few options.
Set the Initial State
The most naive approach for pre-populating your global store is to set the initial state when you create your store:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
user: { name: "Austin" }
},
mutations: {
setUser (user) {
state.user = user
}
}
})
Obviously this only works if you know ahead of time the details about the user. When we are building out application, we probably won’t know the user’s name, but there is another option.
We can take advantage of localStorage
to keep a copy of the user’s information, however. When they sign in, you set the details in localStorage
, and when they log our, you remove the details from localStorage
.
When the app loads, you can pull the user details from localStorage
and into the initial state:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
user: localStorage.get('user')
},
mutations: {
setUser (user) {
state.user = user
}
}
})
If you’re working with data that does not require super tight security restrictions, then this works pretty well. I would recommend the vuex-persistedstate
library to help automate that.
Keep in mind that you should never store very sensitive data like auth tokens in localStorage
because it can be targeted by XSS attacks. So our example works alright for a user’s name, but not for something like an auth token. Those should only be store in memory (which can still be Vuex, just not persisted).
Request Data When the App Mounts
Now let’s say for whatever reason we don’t want to store data in localStorage
. Our next option might be to leave our initial state empty and allow our application to mount. Once the app has mounted, we can make some HTTP request to our server to get our data, then update the global state:
<template>
<div>
<p v-if="user">Hi {{ user.name }}, welcome back!</p>
<p v-else>You should probably log in.</p>
</div>
</template>
<script>
export default {
computed {
user() {
return this.$store.state.user
}
},
async mounted() {
const user = await getUser() // Assume getUser returns a user object with a name property
this.$store.commit('setUser', user)
}
}
</script>
This works fine, but now we have a weird user experience. The application will load and send off the request, but while the user is waiting for the request to come back, they are seeing the “You should probably log in.” When the request returns, assuming they have a logged in session, that message quickly changes to “Hi {{ user.name }}
, welcome back!”. This flash can look janky.
To fix this flash we can simply show a loading element while the request is out:
<template>
<div>
<p v-if="loading">Loading...</p>
<p v-else-if="user">Hi {{ user.name }}, welcome back!</p>
<p v-else>You should probably log in.</p>
</div>
</template>
<script>
export default {
data: () => ({
loading: false
}),
computed {
user() {
return this.$store.state.user
}
},
async mounted() {
this.loading = true
const user = await fetch('/user').then(r => r.json()) // Assume getUser returns a user object with a name property
this.$store.commit('setUser', user)
this.loading = false
}
}
</script>
Keep in mind that this is a very bare example. In yours, you might have a dedicated component for loading animations, and you may have a <router-view>
component in place of the user messages here. You may also choose to make that HTTP request from a Vuex action. The concept still applies.
Request Data Before App Loads
The last example I’ll look at is making HTTP requests similar to the last, but waiting for the request to return and updating the store before the application ever has a chance to load.
If we keep in mind that a Vuex store is just an object with some properties and methods, we can treat it the same as any other JavaScript object.
We can import our store into our main.js
file (or whatever the entry point for your application is) and invoke our HTTP request before mounting the application:
import Vue from "vue"
import store from "./store"
import App from "./App.vue"
fetch('/user')
.then(r => r.json())
.then((user) => {
store.commit('setUser', user)
new Vue({
store,
render: (h) => h(App),
}).$mount("#app")
})
.catch((error) => {
// Don't forget to handle this
})
This approach has the benefit of preloading your global store with any data it would need to get from an API before the application loads. This is a convenient way to avoid the previously mentioned issues of janky jumps or managing some loading logic.
However…
There is a major caveat here. It’s true that you don’t have to worry about showing a loading spinner while the HTTP request is pending, but in the meantime, nothing in your app is showing. If your app is a single page application, then your user could be stuck staring at a blank white page until the request returns.
So you aren’t really solving a latency problem, just deciding what sort of UI experience to show while your waiting on data.
Closing Remarks
I don’t have any hard, fast rules on which method here is best. In reality, you may use all three depending on what the data is that you are fetching, and what your application needs are.
I should also mention that although my examples I’m making fetch
requests then using Vuex mutations to commit to the store directly. You could just as easily use Vuex actions to implement the fetch
. You could also apply these same principles to any other state management tool, such as Vue.observable.
Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to share it, sign up for my newsletter, and follow me on Twitter.
Originally published on austingil.com.