Pinia State Management in the API3 Ecosystem
Building API3’s Ecosystem hub was a balancing act between user experience, performance, and real-time data interactions. With so much going on—complex filters, responsive layouts, and Web3 connectivity—keeping everything in sync was essential. Pinia quickly became my go-to for structuring state across the site. Here’s a peek into how each store was set up and how it made the entire ecosystem feel fast, interactive, and cohesive.
Why Pinia?
With Pinia, each feature of the site could have its own dedicated store, handling its own piece of the puzzle without cluttering the codebase. This approach kept things modular, clear, and scalable. From filtering dApps to managing user interactions, Pinia’s reactivity and modularity were the backbone that held it all together.
Behind the Scenes: API3’s Key Pinia Stores
Here’s how each store came together to create an engaging experience.
1. Ecosystem Store: The Data Hub
The ecosystem.js
store is the core of all data-driven interactions. It pulls in project lists, keeps track of user-selected filters, and handles pagination. This store is the real MVP behind the DappFilter
and DappGrid
components, ensuring that users can search, filter, and load content without missing a beat.
// ecosystem.js - Ecosystem Store
import { defineStore } from "pinia";
import { useFetch } from "nuxt/app";
export const useEcosystemStore = defineStore("ecosystem", () => {
const filterQuery = ref({
searchKey: "",
chains: {},
categories: {},
productTypes: {},
page: 1,
});
const projectList = ref([]);
const hasMoreItems = ref(true);
const serverURL = computed(() => {
let url = `/api/projects/?page=${filterQuery.value.page}`;
url += filterQuery.value.searchKey ? `&search=${filterQuery.value.searchKey}` : "";
return url;
});
const { data } = useFetch(() => serverURL.value);
watch([filterQuery, data], ([newQuery, newData]) => {
if (!newData) return;
projectList.value = newQuery.page === 1 ? newData.projects : [...projectList.value, ...newData.projects];
hasMoreItems.value = newData.projects.length >= 10;
});
return { list: projectList, filterQuery, hasMoreItems };
});
In the ecosystem, this store synchronizes with the DappFilter
component, which enables users to refine their search by networks, product types, categories, and more. The filter options are dynamically populated based on ecosystem.stats
, a dataset that allows the filter criteria to expand and adapt. For instance:
const categories = computed(() => {
if (ecosystem.stats) {
return [...ecosystem.stats.categories];
}
});
The DappGrid
component then uses this store’s pagination capabilities to display filtered results, enabling an infinite scroll experience with the help of GSAP ScrollTrigger
. This setup lets users explore the dApp ecosystem with zero friction, as new items load only when needed.
2. Blog Store: Managing Content with Dynamic Pagination
The blog.js
store handles the articles section of the site, managing both pagination and dynamic sorting to keep everything organized and relevant. This store powers the ArticleGrid
component, which displays articles in multiple grid layouts based on user preferences.
// blog.js - Blog Store
import { defineStore } from "pinia";
export const useBlogStore = defineStore("blog", () => {
const filterQuery = ref({ searchKey: "", page: 1 });
const articlesList = ref([]);
const hasMoreItems = ref(true);
const serverURL = computed(() => `/api/articles/?page=${filterQuery.value.page}`);
const { data } = useFetch(() => serverURL.value);
watch([filterQuery, data], ([newQuery, newData]) => {
if (!newData) return;
articlesList.value = newQuery.page === 1 ? newData.articles : [...articlesList.value, ...newData.articles];
hasMoreItems.value = newData.articles.length >= 10;
});
return { list: articlesList, filterQuery, hasMoreItems };
});
ArticleGrid
takes this data and organizes it with dynamic layouts based on props like isRecentSort
or isPopularSort
. The sorted
computed property ensures that the latest, most popular, or trending content is always front and center.
const sorted = computed(() => {
if (blog.list && props.isRecentSort) {
return blog.list.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
}
if (blog.list && props.isPopularSort) {
return blog.list.sort((a, b) => (b.views ?? 0) - (a.views ?? 0));
}
return blog.list.filter((article) => !article.hidden);
});
Thanks to Pinia’s reactivity, this store keeps the ArticleGrid
responsive, instantly updating whenever filters or sorting options change, giving users an experience that feels polished and frictionless.
3. Interface Store: Centralized UI State
The interface.js
store is responsible for all global UI elements, like modal visibility, viewport tracking, and responsive layouts. This store connects with components throughout the app, from managing modals in DappFilter
to tracking responsive layouts for mobile devices.
// interface.js - Interface Store
import { defineStore } from "pinia";
export const useInterfaceStore = defineStore("interface", () => {
const isMobile = ref(false);
const mainMenuOpen = ref(false);
function toggleMenu() {
mainMenuOpen.value = !mainMenuOpen.value;
}
function updateViewportSize() {
isMobile.value = window.innerWidth < 768;
}
onMounted(() => {
updateViewportSize();
window.addEventListener("resize", updateViewportSize);
});
return { isMobile, mainMenuOpen, toggleMenu };
});
The DappFilter
component also relies on this store to provide mobile-specific actions, including modal controls for applying or canceling filters. This approach enables users to navigate the site effortlessly, regardless of device size. The store manages responsive breakpoints to adapt the layout dynamically, especially crucial for components like ArticleGrid
, which adjust their layout based on isMobile
or isTablet
flags.
4. Web3 Store: Simplifying Blockchain Interactions
Finally, the web3Store.js
store centralizes all Web3-related interactions, from wallet connections to blockchain switching. This keeps Web3 functionality straightforward and easily accessible across components.
// web3Store.js - Web3 Store
import { defineStore } from "pinia";
export const useWeb3Store = defineStore("web3Store", () => {
const state = ref({ account: null, isConnected: false });
return { state };
});
Finally, the web3Store.js
store centralizes all Web3-related interactions, from wallet connections to blockchain switching. This keeps Web3 functionality straightforward and easily accessible across components.
Wrapping Up
Using Pinia for state management in API3's Ecosystem was transformative. Each store—whether handling data, UI elements, or Web3 functionality—became a self-contained module, making complex functionality simple and ensuring a consistent, performant experience for users. If you're working on a Vue project with a lot of moving parts, consider Pinia’s modular setup; it could be the key to keeping everything in sync and under control.