Skip to content

Build a reactive list

Build a component that uses useListInstance to fetch contacts and render them as stable, ordered rows. One small handler fetches the data. The instance owns the reactive state and keeps every row keyed and ordered as the list changes. This tutorial assumes the package is already installed; see Getting started if it is not.

1. Sketch the contacts source

The list needs somewhere to fetch from. A tiny in-memory array runs this whole page without a backend; the shape is what matters. Each contact has a contactId that acts as its primary key:

javascript
const contactRows = [
    { contactId: 1, name: "Ada Lovelace", email: "ada@example.com" },
    { contactId: 2, name: "Grace Hopper", email: "grace@example.com" },
    { contactId: 3, name: "Mary Jackson", email: "mary@example.com" },
];

async function fetchContacts() {
    // Swap for a real request, e.g. fetch("/api/contacts").then((r) => r.json()).
    return contactRows;
}

2. Create the list instance

Call useListInstance once in your component's setup, naming the primary key field and providing a list handler:

javascript
import { useListInstance } from "@arrai-innovations/reactive-helpers";

const contacts = useListInstance({
    props: { pkKey: "contactId" },
    handlers: {
        list: async ({ pushObjects }) => {
            pushObjects(await fetchContacts());
        },
    },
});
  • pkKey tells the instance which field identifies a contact. Every row is stored under its contactId.
  • handlers.list is your transport. It receives a pushObjects callback and calls it with each batch of results; here that is one call with the whole array.
  • The returned instance pairs reactive state (contacts.state) with the actions that drive it (contacts.list()).

Passing handlers per instance keeps the whole example in one file; Register app-wide CRUD defaults shows how to register one shared data layer instead.

3. Fetch the contacts

Trigger the fetch once during setup:

javascript
contacts.list();

list() runs your handler and resolves to true on success or false on failure. While it runs, contacts.state.loading is true; if the handler throws or rejects, the error lands in contacts.state.error and contacts.state.errored becomes true. Your component never needs its own try/catch.

4. Render the rows

The fetched contacts land in contacts.state.objects, keyed by primary key, and contacts.state.objectsInOrder, a computed array in the order your handler pushed them. Iterate the ordered array and key each row by contactId. The complete component:

vue
<script setup>
import { useListInstance } from "@arrai-innovations/reactive-helpers";

const contactRows = [
    { contactId: 1, name: "Ada Lovelace", email: "ada@example.com" },
    { contactId: 2, name: "Grace Hopper", email: "grace@example.com" },
    { contactId: 3, name: "Mary Jackson", email: "mary@example.com" },
];

async function fetchContacts() {
    // Swap for a real request, e.g. fetch("/api/contacts").then((r) => r.json()).
    return contactRows;
}

const contacts = useListInstance({
    props: { pkKey: "contactId" },
    handlers: {
        list: async ({ pushObjects }) => {
            pushObjects(await fetchContacts());
        },
    },
});

contacts.list();
</script>

<template>
    <p v-if="contacts.state.loading">Loading contacts...</p>
    <p v-else-if="contacts.state.errored" role="alert">{{ contacts.state.error.message }}</p>
    <ul v-else>
        <li v-for="contact in contacts.state.objectsInOrder" :key="contact.contactId">
            {{ contact.name }} ({{ contact.email }})
        </li>
    </ul>
</template>

The component shows the loading message while the handler runs, then swaps to one row per contact. Because every piece of contacts.state is reactive, the template tracks it all with no extra wiring.

What you built

A list instance that fetches through your handler and renders stable, ordered rows. Rows appear in the order the handler pushed them, and each is keyed by its contactId. Call contacts.list() again and rows whose contactId is already there are updated in place rather than recreated. Their position and :key stay stable while their fields change reactively.

Next steps