State Management without Vuex

Published on

In the modern frontend development, we want to create re-usable components that can be shared and used in different components. These components contain the UI needed for rendering and the logic needed to behave correctly. As time goes on, the data, or state, used in one component may be needed in another. For example you send request to get data from endpoint and your child components need it.

Props

One way for parent component to communicate with child component is through props. You can define props on one component which denotes what data can this component accept from parent component.

// Child Component
<template>
  <div class="child">
    {{ items }}
  </div>
</template>

<script>
export default {
  name: "Child",

  props: {
    items: [],
  }
}
</script>

So here we define items to be one of the props that it can accept and we just simply print them on the template. The parent component could be fetching the data needed and pass that data to the child component.

// Parent Component
<template>
  <div class="parent">
    <child-component :items="items" />
  </div>
</template>

<script>
import { onMounted, ref } from "vue";
import ChildComponent from "./Child.vue";

export default {
  name: "Parent",

  components: {
    ChildComponent,
  },

  setup() {
    const items = ref([]);

    onMounted(async () => {
      items.value = await fetch("https://jsonplaceholder.typicode.com/todos").then((res) => res.json());
    });

    return {
      items,
    };
  },
};
</script>

So far the example is ok and you can see such things happen in many applications. But imagine you have child component in child component in child component and so on. It's a tree of components that has many layers. What should we do if the data fetched on the first layer is needed on the 5th layer? You would need to keep passing the props down to the components. The problems are it's hard to tell where this data is coming from and it doesn't look nice to have a prop that this component doesn't need but just need to pass it to other child components.

State Management

Vuex

Vuex is a state management solution that Vue team officially introduced. This is to provide better clarity on where the data is being updated, provide better access for components to get the states, and doing all of these while maintaining reactivity. I think Vuex is all about clarity like we know exactly why the state is updated and where the state is from. It removes that confusion of the why. With the devtool support in Vue Chrome extension, you can even "time travel" in states and get a more understanding on which actions have been fired and how many times. An example of the Vuex store file may look like:

const state = {
  storeCounter: 0,
};

const mutations = {
  incrementCounter(state) {
    state.storeCounter++;
  },
};

export const store = {
  state,
  mutations,
};

A downside for me is there is some boilerplate I need to set up for each store module. You can have a file dedicated to product page, catalog page, or a file to share global states throughout the whole application and you have to set up the file before you can use it. To update the state you have to define the state, write the mutation to update that state, and import the state in the component. In my opinion it's sacrificing efficiency for clarity. Some people like it, I personally hope there is a better way.

You can check out my other post with a Vue 3 starter project. It shows you how to include a Vuex store in your Vue app.

Composition API

And I think I just found a better way. Here comes Composition API. It's a way to organize our code in a more contextual manner and can still share our code. I wrote about it when I went to Vue Toronto Conference in 2019: Vue 3 and Composition API.

import { ref } from "vue";

export default function useCounter() {
  const counter = ref(0);

  function incrementCounter() {
    counter.value++;
  }

  return {
    counter,
    incrementCounter,
  };
}

And then you can use it:

<template>
  <div class="counter">
    <button @click="incrementCounter">Increment</button>

    Composition Counter: {{counter}}
  </div>
</template>

<script>
import useCounter from "useCounter";

export default {
  setup() {
    const { counter, incrementCounter } = useCounter();

    return { counter, incrementCounter, godcode };
  },
}
</script>

If you use the example code above directly, it doesn't really share the states but share the logic. So if you have multiple components that require counter and maintain its own state, this code can do that. But what if I want to share the state for every component that uses this composition function? I just move the ref value out of the function:

import { ref } from "vue";

const counter = ref(0);

export default function useCounter() {

  function incrementCounter() {
    counter.value++;
  }

  return {
    counter,
    incrementCounter,
  };
}

Now the ref value is not bound inside the scope if the useCounter function but globally in the file. Now if multiple components import and use this composition function, they will all share the same counter state. With that, it does the same thing that Vuex provides in a more intuitive JavaScript way instead of more Vue way.

I personally like this approach, and we are using it during Vue server side rendering migration, because it's more intuitive as just mentioned and also it doesn't depend on Vuex so it is one less module we need to install. We are still experimenting with this approach but so far every developer seems to be happier than before.

One problem we saw with this approach was during server-side rendering, the states would be kept alive globally across different sessions. But we had a solution to that so that's probably another blog post.

Conclusion

A lot of people have the question of when do you actually need a state management. The answer is a bit philosophical. You will know it when the time comes. When you start to write the same code again and again plus you need to keep defining the states or you want to get that state from another component, then you know you probably need a solution for this.