State in Vue
Like most JS frameworks Vue supports state. And like the others Vue has component & global state.
Let's build a Todo list to learn how to CRUD using local & global state in Vue.
Start by defining the TodosContainer.vue
component.
Start with defining your state vars.
The newTodo
var will hold the new todo we're adding & todos
our list of todos.
./components/Todos/TodosContainer.vue
vue <script setup>
const todos = ref([])
const newTodo = ref('')
</script>
<template>
<div>
<h1>Todos</h1>
</div>
</template>
Next add an input to update newTodo
.
Update newTodo
when the input changes by binding it to the input using v-model
./components/Todos/TodosContainer.vue
vue <script setup>
const todos = ref([])
const newTodo = ref('')
</script>
<template>
<div>
<h1>Todos</h1>
<input
autofocus
v-model="newTodo"
class="text-black px-1"
/>
</div>
</template>
Next define a handler, addTodo
, that implements the logic of adding a todo
to our todos
list.
./components/Todos/TodosContainer.vue
vue <script setup>
const todos = ref([])
const newTodo = ref('')
function addTodo() {
const todo = {
done: false,
name: newTodo.value,
id: todos.value.length + 1,
}
todos.value.push(todo)
newTodo.value = ''
}
</script>
<template>
<div>
<h1>Todos</h1>
<input
autofocus
v-model="newTodo"
class="text-black px-1"
/>
</div>
</template>
And lastly trigger addTodo
when the user hits the enter key by binding addTodo
to the enter key up event.
./components/Todos/TodosContainer.vue
vue <script setup>
const todos = ref([])
const newTodo = ref('')
function addTodo() {
const todo = {
done: false,
name: newTodo.value,
id: todos.value.length + 1,
}
todos.value.push(todo)
newTodo.value = ''
}
</script>
<template>
<div>
<h1>Todos</h1>
<input
autofocus
v-model="newTodo"
class="text-black px-1"
@keyup.enter="addTodo" +!!!
/>
</div>
</template>
You'll now see todos
update when you enter a new todo and press enter in your console.
Next use a v-for
to render the todos
.
./components/Todos/TodosContainer.vue
vue <script setup>
const todos = ref([])
const newTodo = ref('')
function addTodo() {
const todo = {
done: false,
name: newTodo.value,
id: todos.value.length + 1,
}
todos.value.push(todo)
newTodo.value = ''
}
</script>
<template>
<div>
<h1>Todos</h1>
<input
autofocus
v-model="newTodo"
class="text-black px-1"
@keyup.enter="addTodo"
/>
<ul>
<li
:key="todo.id"
v-for="todo of todos"
class="flex flex-row justify-between"
>
<span v-text="todo.name" />
</li>
</ul>
</div>
</template>
Next implement the ability to add a todo by defining the handler & a state var newTodo
to hold the name of the todo
Now let's add the ability to update a todo.
Define a function to toggle the status of a todo, toggleStatus
.
./components/Todos/TodosContainer.vue
vue <script setup>
const todos = ref([])
const newTodo = ref('')
function addTodo() {
const todo = {
done: false,
name: newTodo.value,
id: todos.value.length + 1,
}
todos.value.push(todo)
newTodo.value = ''
}
function toggleStatus(id) {
const idx = todos.value.findIndex((t) => t.id === id)
const todo = todos.value[idx]
todo.done = !todo.done
todos.value[idx] = todo
}
</script>
<template>
<div>
<h1>Todos</h1>
<input
autofocus
v-model="newTodo"
class="text-black px-1"
@keyup.enter="addTodo"
/>
<ul>
<li
:key="todo.id"
v-for="todo of todos"
class="flex flex-row justify-between"
>
<span v-text="todo.name" />
</li>
</ul>
</div>
</template>
Bind toggleStatus
to the @click
of each todo item.
Also make sure to pass the id of the todo to toggleStatus
as well.
./components/Todos/TodosContainer.vue
vue <script setup>
const todos = ref([])
const newTodo = ref('')
function addTodo() {
const todo = {
done: false,
name: newTodo.value,
id: todos.value.length + 1,
}
todos.value.push(todo)
newTodo.value = ''
}
function toggleStatus(id) {
const idx = todos.value.findIndex((t) => t.id === id)
const todo = todos.value[idx]
todo.done = !todo.done
todos.value[idx] = todo
}
</script>
<template>
<div>
<h1>Todos</h1>
<input
autofocus
v-model="newTodo"
class="text-black px-1"
@keyup.enter="addTodo"
/>
<ul>
<li
:key="todo.id"
v-for="todo of todos"
@click="toggleStatus(todo.id)" +!!!
class="flex flex-row justify-between"
>
<span v-text="todo.name" />
</li>
</ul>
</div>
</template>
Lastly programmatically add the class .done
to each todo item.
Also define a class .done
in the style tag which will give the todo a line-through if it's status is done.
./components/Todos/TodosContainer.vue
vue <script setup>
const todos = ref([])
const newTodo = ref('')
function addTodo() {
const todo = {
done: false,
name: newTodo.value,
id: todos.value.length + 1,
}
todos.value.push(todo)
newTodo.value = ''
}
function toggleStatus(id) {
const idx = todos.value.findIndex((t) => t.id === id)
const todo = todos.value[idx]
todo.done = !todo.done
todos.value[idx] = todo
}
</script>
<template>
<div>
<h1>Todos</h1>
<input
autofocus
v-model="newTodo"
class="text-black px-1"
@keyup.enter="addTodo"
/>
<ul>
<li
:key="todo.id"
v-for="todo of todos"
:class="{ done: todo.done }"
@click="toggleStatus(todo.id)"
class="flex flex-row justify-between"
>
<span v-text="todo.name" />
</li>
</ul>
</div>
</template>
<style>
.done {
color: indianred;
text-decoration: line-through;
}
</style>
The last thing we need to do is add the ability to remove a todo item.
Define removeTodo
which finds a todo in the list via id and removes it from the list.
./components/Todos/TodosContainer.vue
vue <script setup>
const todos = ref([])
const newTodo = ref('')
function addTodo() {
const todo = {
done: false,
name: newTodo.value,
id: todos.value.length + 1,
}
todos.value.push(todo)
newTodo.value = ''
}
function toggleStatus(id) {
const idx = todos.value.findIndex((t) => t.id === id)
const todo = todos.value[idx]
todo.done = !todo.done
todos.value[idx] = todo
}
function removeTodo(id) {
const idx = todos.value.findIndex((t) => t.id === id)
todos.value.splice(idx, 1)
}
</script>
<template>
<div>
<h1>Todos</h1>
<input
autofocus
v-model="newTodo"
class="text-black px-1"
@keyup.enter="addTodo"
/>
<ul>
<li
:key="todo.id"
v-for="todo of todos"
:class="{ done: todo.done }"
@click="toggleStatus(todo.id)"
class="flex flex-row justify-between"
>
<span v-text="todo.name" />
</li>
</ul>
</div>
</template>
<style>
.done {
color: indianred;
text-decoration: line-through;
}
</style>
Bind removeTodo
to an @click
event of an HTML element of your choice.
I imported & used an icon here.
Once @click
of the todo item is triggered you'll see the todo removed from the list.
./components/Todos/TodosContainer.vue
vue <script setup>
import XIcon from '~/assets/images/icons/XIcon.vue'
const todos = ref([])
const newTodo = ref('')
function addTodo() {
const todo = {
done: false,
name: newTodo.value,
id: todos.value.length + 1,
}
todos.value.push(todo)
newTodo.value = ''
}
function toggleStatus(id) {
const idx = todos.value.findIndex((t) => t.id === id)
const todo = todos.value[idx]
todo.done = !todo.done
todos.value[idx] = todo
}
function removeTodo(id) {
const idx = todos.value.findIndex((t) => t.id === id)
todos.value.splice(idx, 1)
}
</script>
<template>
<div>
<h1>Todos</h1>
<input
autofocus
v-model="newTodo"
class="text-black px-1"
@keyup.enter="addTodo"
/>
<ul>
<li
:key="todo.id"
v-for="todo of todos"
:class="{ done: todo.done }"
@click="toggleStatus(todo.id)"
class="flex flex-row justify-between"
>
<span v-text="todo.name" />
<XIcon @click="removeTodo(todo.id)" />
</li>
</ul>
</div>
</template>
<style>
.done {
color: indianred;
text-decoration: line-through;
}
</style>
It's often the case that state needs to be shared throughout the application.
To achieve this a parent component could define state and pass it to it's children using props.
This works but it creates a few problems:
- The parent component bloats.
- Components are now tightly coupled.
- Components now have parent/child hierarchy.
Vue has a better solution:
The useState
method is provided by Vue.
To refactor todos
to a global state do the following.
Refactor the initialization of todos
to the following:
./components/Todos/TodosContainer.vue
vue <script setup>
const todos = ref([]) -!!!
const todos = useState('todos', () => []) +!!!
// etc...
</script>
<template>
<!-- etc... -->
</template>
<style>
/* etc... */
</style>
Now you can use the same state in other components by calling useState()
again.
This case for example, you grab the todos and count the done and undone to provide additional context to the user.
./components/Todos/TodosMeta.vue
vue <script setup>
const todos = useState('todos', () => [])
const countDone = computed(() => todos.value.filter((t) => t.done).length)
const countUndone = computed(() => todos.value.filter((t) => !t.done).length)
</script>
<template>
<div>
<div>
<label>Done Count</label>
<div>
<span v-text="countDone" />
</div>
</div>
<div>
<label>Undone Count</label>
<div>
<span v-text="countUndone" />
</div>
</div>
</div>
</template>
Now you'll see that when you add a todo
to your todos
list then both components, TodosMeta
& TodosContainer
update.