Letās get some state managedĀ š
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:
Step 1: Our first app. ElectionĀ Day!
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.
Step 2: Isolating state
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:
- Look maā, no props! (lines 8, 9)
- Every component registers our state in their
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) - We had to remove our pretty arrow function from the render functions because now we are using
this
. (lines 21, 28) - Now our state it āisolatedā from components. Free as in beer. (line 14)
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:
Step 3: Create a shared Vue instance to hold that reactivity
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:
- State and methods that mutate our state are now placed together. No more leaking implementation details! Notice that our voteFor methods are quite simple, but that they could be as complicated as needed. (lines 9, 10)
- We still need to call these methods from our component. (lines 25, 26)
- Back to our render functions with arrows. (lines 15, 19)
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ā¦
Step 4: Encapsulate our state in aĀ function
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:
- A data parameter to set our initial state. We could call the parameter āstateā, for the sake of clarity.
- A list of mutations functions to change my state when needed. We could call the parameter āmutationsā, for the sake of clarity.
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:
- First of all, we return a new Vue instance. So far so good.
- Then, we register our state parameter to the
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 callcommit('someMutation')
, our method will callmutations.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
š.
ā Thatās aĀ wrap!
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:
- State management on the front end is something scalable. My personal recommendation: start as small as possible, and think about it twice before adding new things. Vuex is amazing (it truly is!), but do you really need it yet?
- Reactivity is the king of Vue. Everything, and I mean everything, depends on data being reactive. And this is great because we can leverage that reactivity and create nice, useful abstractions.
- Now we kinda understand what Vuex is doing under the hood, which is cool.
- Sometimes, verbosity trumps succinctness if it provides context, intent, and repeatability to our code (for instance, step 4 required way more code that step 2).
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!