In the previous chapter, Web Request Router & SSE, we built a high-speed data pipeline. The backend is now broadcasting real-time updates about containers using Server-Sent Events (SSE).
However, broadcasting data is only half the battle. We need something in the browser to catch that data, organize it, and display it to the user.
In this chapter, we will build the Frontend State Management system using Pinia.
Think of Dozzle like a modern car's dashboard.
If the engine speeds up, the computer updates the value, and the speedometer needle moves instantly. The needle doesn't need to ask the engine "How fast are we?" every second. It just reacts to the computer.
Without a central store, our application would be chaotic.
Imagine we have three components on the screen:
If a container stops, the Backend sends one message. Without a store, we would have to manually tell the Sidebar to delete the name, the Main Area to stop the graph, and the Header to subtract 1. This is messy and bug-prone.
We use Pinia, the official state management library for Vue.js.
Pinia acts as a "Single Source of Truth."
In assets/stores/container.ts, we define our store. It's essentially a smart container for our variables.
We need a list to hold our containers. In Vue/Pinia, we use ref to make this list reactive.
// assets/stores/container.ts
export const useContainerStore = defineStore("container", () => {
// This is our database in the browser's memory
const containers = ref<Container[]>([]);
// A flag to know if we are connected to the backend
const ready = ref(false);
return { containers, ready };
});
Beginner Note:
ref([])creates a special array. When you modify this array, Vue automatically detects the change and updates the HTML of the webpage.
Now we need to plug the Store into the SSE stream we created in the previous chapter. We use the browser's native EventSource API.
Inside the store, we create a connect() function:
// assets/stores/container.ts
function connect() {
// Connect to the route we defined in Chapter 4
const es = new EventSource("/api/events/stream");
// When the connection opens, clear the list
es.onopen = () => {
containers.value = [];
ready.value = true;
};
// ... listeners go here
}
The backend sends different types of events (e.g., "container-event", "container-stat"). We add listeners for each one.
When a container starts or dies, the backend sends a container-event.
// assets/stores/container.ts
es.addEventListener("container-event", (e) => {
// 1. Parse the text data from the backend into an Object
const event = JSON.parse(e.data);
// 2. Find the specific container in our list
const container = findContainerById(event.actorId);
// 3. Update its state (e.g., "running" -> "exited")
if (container) {
container.state = event.name === "die" ? "exited" : "running";
}
});
Because container is part of our reactive list, changing .state here immediately turns the status icon from Green to Red on the screen!
Sometimes the UI needs a filtered version of the data. For example, "Show me only running containers."
Instead of creating a new list, we use computed.
// assets/stores/container.ts
const visibleContainers = computed(() => {
// This function runs automatically whenever 'containers' changes
return containers.value.filter(c => c.state === "running");
});
Let's visualize the data flow from the moment a container crashes to the moment the user sees it.
Now let's look at how a UI component uses this. Open assets/pages/index.vue.
The component doesn't care about WebSockets, APIs, or JSON parsing. It just asks the store for data.
<!-- assets/pages/index.vue -->
<script setup lang="ts">
// 1. Get access to the store
const containerStore = useContainerStore();
// 2. Extract the reactive list of containers
const { containers } = storeToRefs(containerStore);
// 3. Create a simple count for the header
const runningCount = computed(() =>
containers.value.filter(c => c.state === "running").length
);
</script>
<template>
<!-- 4. Use the data directly in HTML -->
<h1>Running: {{ runningCount }}</h1>
<!-- Pass the data to the table component -->
<ContainerTable :containers="containers" />
</template>
In assets/components/ContainerTable.vue, we display the data. Notice how cleaner the code is because the complex logic is hidden in the Store.
<!-- assets/components/ContainerTable.vue -->
<template>
<table>
<tr v-for="container in containers" :key="container.id">
<!-- We just read properties. If they change, this text changes. -->
<td>{{ container.name }}</td>
<td>{{ container.state }}</td>
<td>
<!-- Even complex objects like CPU stats update live -->
<ContainerStatCell :container="container" type="cpu" />
</td>
</tr>
</table>
</template>
EventSource keeps the Store fresh, and the Store keeps the UI fresh.You have successfully built the brain of the frontend!
Now our dashboard is alive. But currently, it's "Read-Only." We can see what containers are doing, but we can't control them. In the next chapter, we will learn how to send commands back to the server to restart or stop containers.
Next Chapter: Agent & RPC System
Generated by Code IQ