There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton
Well, I guess Phil Karlton never had to deal with managing state on the front end..!
State management is one of “those things”. Backends roll their eyes, frontends hide under the desk. After all, managing state is the hardest part of being a frontend developer: you need to think in terms of the UI as something that changes over time. And we are not particularly good at it.
In this post, we will discover how to handle state in a Vue application from the ground up. We will end up creating our own state manager generator!
Let’s dive in:
First of all, we need an application. We cannot manage an application state without an application, right?
Let’s create a voting app, to let you folks vote for the next President(?):
Yes, I merged Single File Components and inline components.
TODO (REMOVE BEFORE PUBLISHING): avoid making jokes about politics. Not a good time, not a good time.
The code above renders something as pretty as this:
It looks like the browser failed to load the CSS
I can hear your brain screaming:
“Man, you are not managing state. You are just passing props down to each component. You promised state management. You better deliver”.
Well, isn’t passing props the simplest form of “state management”? Isn’t our main component holding both red
and blue
, our pieces of state?
(The answers are YES and YES)
But yeah, I hear you. Passing down props is not pretty nor comfortable nor scalable, so let’s try something else.
Let’s create a “state holder” object and manage our whole state from there.
const state = {red: 0,blue: 0,}
There it is! Our application state, properly held and encapsulated. It wasn’t that hard!
Now, from our components, we could do something like the following:
const TotalVotes = {render: h => h('div', `Total votes: ${state.red + state.blue}`)}
const Results = {render: h => h('div', `Red: ${state.red} - Blue: ${state.blue}`),}
// ...and, inside our main component,...methods: {voteForRed () { state.red++ },voteForBlue () { state.blue++ },},
Spoiler: this is not going to work. Why?
Because Vue uses data
method to trigger its “magic reactivity”. Without passing our data to data
(heh), Vue won’t be able to track down value changes and update our components in response.
Easily said, easily(?) fixed:
A few things happened there:
data
method. Now Vue is able to track down state changes, so when we vote for 🔴 all our components rerender with the proper value. (lines 20, 27, 35)this
. (lines 21, 28)Ok, so now we have our state separated from our “UI implementation”, but that came with some caveats: we need to register our state to each component in data()
, we cannot use the beautiful arrow functions in our render functions…
But.
Wait.
Did I just say “Vue needs to register data in _data()_
to make it reactive?”.
Yes, I did.
But in my solution I’m using every component instance to make the very same data reactive, right?
Yes.
And could I create a shared Vue instance to hold that reactivity, so my components don’t have to?
Well, yes. Let me write a big heading:
So, information stored in data()
becomes “reactive by default”. And what is the piece of information we want to make reactive?
Our state!
So what if we did this?
const state = new Vue({data () {return {red: 0,blue: 0,}},})
Neat! Now our state is reactive. We’ll be sharing a Vue instance for all the data, but that’ll be waaaay cleaner than my previous solution, right?
But, wait. Wait. Wait. We have a Vue instance, now. And do you know what a Vue instance can hold, besides reactive data?
Exactly: methods.
Now our voteforRed()
and voteForBlue()
methods can be collocated with our state!
Let’s check it out:
Vuetiful! Let me highlight the improvements we achieved:
And we removed a lot of boilerplate code (all the data()
declarations).
Okay, so far so good! Our current solution is terse, simple, and idiomatic.
But we need to import Vue, and then create a new instance. While this is not inherently “bad”, I feel we could do better, couldn’t we?
For instance, our solution cannot be shared among projects right now. I need to teach people to create a Vue instance, populate its data
method, then register some methods to modify the state… way too much.
It’s time to…
Fortunately, Javascript provides us with a cool feature that allows us to hide all those details and keep things simple: functions. We are gonna create our factory function.
Let’s define our createStore
function. What’s the API? I would expect:
Finally, I would expect our createStore
to expose a generic method that would allow my components to “run” the mutations. We could call the parameter “commit”, for the sake of clarity (you usually commit mutations, right?).
You see where I’m going, don’t ya.
We want to end up writing this:
const store = createStore({state: { red: 0, blue: 0 },mutations: {voteForRed (state) { state.red++ },voteForBlue (state) { state.blue++ },},})
Quite nice, right? And pretty straightforward.
Now, how would we implement this createStore
helper? Remember that we should use a Vue instance to leverage its reactivity:
const createStore = ({ state, mutations }) =>new Vue({data () {return { state }},methods: {commit (mutationName) {mutations[mutationName](this.state)},},})
Some things happened there:
data()
method of the instance. Bam! Our state is now reactive.
Finally, we create our public commit()
method. This method takes a name of a mutation as the parameter, and then runs the very same mutation (and passes our state). If we call commit('someMutation')
, our method will call mutations.someMutation(this.state)
.Notice that in a real implementation we should handle non-existent mutations!
So, how do our component look like, now?
const TotalVotes = {render: h => h('div', `Total votes: ${store.state.red + store.state.blue}`),}
const Results = {render: h => h('div', `Red: ${store.state.red} - Blue: ${store.state.blue}`),}
export default {components: { TotalVotes, Results },methods: {voteForRed () { store.commit('voteForRed') },voteForBlue () { store.commit('voteForBlue') },},}
Now we access store.state
to get our state, and store.commit
to modify it (notice that we pass the desired mutation name as parameter).
All together now!:
Isn’t that cool?
Now we can generate hundreds of thousands of stores by providing a simple createStore
method. You’d want to place your createStore
in a file and export it, so you can import it in your applications and create a whole new store. Bonus points if you call this file Vuex.js
😁.
state
, mutations
… does it sound familiar to you? Well, if you have ever used Vuex, it definitely should. We effectively mapped the Vuex API in our example.
We are missing getters and actions, but I hope you get the idea that Vuex is an abstraction of things we already knew. It’s a great abstraction, well-polished, useful, scalable. But an abstraction, after all. We just keep adding layers to the heart of the framework: reactivity. That’s the core feature that triggers everything.
Vuex is an abstraction of things we already knew.
A quick recap:
Wanna dig in? I created a Github repo with 4 commits: one commit per step of the post. Feel free to play with it and inspect every change.
Do you want to practice a bit with our solution? Here’s a challenge: How would you implement getters
? And actions
? and… modules? 😏
Hope it helps!