Component State with Composables
Published
03 November 2024Tags
When handling a reactive state with VueJS, there are several available options that can be used regarding the usage and scope of the state.
First, using ref and reactive that declared inside the component or composable scripts to handle local state level. This state can be passed between parent and child component using props
and emits
. The cons using this practice is when the component become more complex and we need to pass through layers of component, props drilling is inevitable.
Provide/inject can be another option to handle reactive state and solve the props drilling issue. Provide/inject is using key and value model to pass the state from parent into child or grandchild and the otherwise. This quite tricky because the registered key only can be used by one component that invoke it. This may cause bug if we render components using v-for
.
The other option is use state management library such as Vuex or Pinia. This may be the ideal way to handle and manage states inside your app. Not only for local component level, but also the entire app. Yet, the only cons this practice has is when it only comes for local component level or between parent to child or grandchild communication, it may quite costly to use this.
So what is the best option?
I found an interesting tweet from @MichaelThiessen about design pattern in Vue. The idea is we create a global state singleton inside a composable scripts and expose it.
The problem when using a composable is states that declared inside the script, especially inside the export
block is not exposed well if we define the composable in several components. But by using this idea, it can be solved.
I'll provide the sample code to make it easier to understand. Say, we have a simple Vue page code.
<template>
<section class="container">
<div class="rect-wrapper">
<template v-for="item in blocks" :key="item.order">
<div :class="['rectangle', item.color]"></div>
</template>
</div>
<button @click="reorder">Reorder</button>
</section>
</template>
<script setup>
import { ref, computed } from 'vue'
const blocks = ref([
{
color: 'yellow',
order: 5
},
{
color: 'red',
order: 3
},
{
color: 'blue',
order: 1
},
{
color: 'green',
order: 2
},
{
color: 'brown',
order: 4
}
])
const len = computed(() => blocks.value.length)
function reorder() {
for (let i = 0; i < len.value; i++) {
for (let j = 0; j < len.value - i - 1; j++) {
if (questions.value[j].order > questions.value[j + 1].order) {
let temp = questions.value[j]
questions.value[j] = questions.value[j + 1]
questions.value[j + 1] = temp
}
}
}
}
</script>
Then, we want to refactor it to be more clean and easy to maintained. The idea here, code inside script will be in a composable script, button and the looping block components split into an individual component. We strict the individual components to do not have any props as we will use the state from composable script.
First, the composable script become like this.
import { computed, reactive, toRefs } from 'vue'
const state = reactive({
blocks: [
{
color: 'yellow',
order: 5
},
{
color: 'red',
order: 3
},
{
color: 'blue',
order: 1
},
{
color: 'green',
order: 2
},
{
color: 'brown',
order: 4
}
]
})
export function useOrders() {
const { blocks } = toRefs(state)
const len = computed(() => blocks.value.length)
function reorder() {
for (let i = 0; i < len.value; i++) {
for (let j = 0; j < len.value - i - 1; j++) {
if (blocks.value[j].order > blocks.value[j + 1].order) {
let temp = blocks.value[j]
blocks.value[j] = blocks.value[j + 1]
blocks.value[j + 1] = temp
}
}
}
}
return {
questions,
reorder,
}
}
As you can see, there is a variable named state
outside the export
block. This is the global state singleton that I mentioned before. Since it outside the export
block, means it access does not scoped only inside the block code. I'll explain you what it means by providing the seperated components.
RectangleBlocks.vue
<script setup>
import { useOrders } from '../composables/useOrders'
const { questions } = useOrders()
</script>
<template>
<div class="rect-wrapper">
<template v-for="item in questions" :key="item.order">
<div :class="['rectangle', item.color]"></div>
</template>
</div>
</template>
ReorderButton.vue
<script setup lang="ts">
import { useOrders } from '../composables/useOrders'
const { reorder } = useOrders()
</script>
<template>
<button @click="reorder">Reorder</button>
</template>
Then, lastly we put them all together in our page script.
<script setup>
import RectangleBlocks from './components/RectangleBlocks.vue'
import ReorderButton from './components/ReorderButton.vue'
</script>
<template>
<section class="container">
<RectangleBlocks />
<ReorderButton />
</section>
</template>
As you can see, there is no need to pass the state or events from parent to child (or otherwise). We fully control the state from composable script.
You can observe and try the code in this sandbox.