Skip to content

Implementing the defineStore Function ​

Following our implementation of createPinia, which provides the central Pinia instance, we now turn our attention to defineStore. This function is used to define state, getters, and actions. Crucially, defineStore does not immediately create a store instance; instead, it returns a function (often named use...Store) that you call within your components to get the actual, active store instance.

The official documentation illustrates its basic usage:

typescript
import { defineStore } from 'pinia'

// You can name the return value of `defineStore()` anything you want,
// but it's best to use the name of the store and surround it with `use`
// and `Store` (e.g. `useUserStore`, `useCartStore`, `useProductStore`)
// the first argument is a unique id of the store across your application
export const useAlertsStore = defineStore('alerts', {
  // other options...
})

As seen in the official usage example (useAlertsStore), the convention is to name the returned function using the use...Store pattern, similar to Vue 3 Composables. This naming hints at its usage as a hook that needs to be called within the setup context of a component or other composable.

In this section, we will implement a simplified version of defineStore, focusing on the Composition API style (where the second argument is a setup function). We will connect it to the Pinia instance created earlier and demonstrate how this pattern allows us to create and use global state effectively. Accurate TypeScript typings, while essential for a robust library, will be addressed in the next section for clarity and focus.

The Composition API Style (setup function) ​

defineStore supports two main ways of defining a store's logic: the Composition API style (using a setup function) and the Options API style (using an options object). In this section, we focus on the Composition API style, as it aligns well with modern Vue 3 development and the structure of our imitation. The Options API style will be covered later.

When you call defineStore(id, setup), two main things happen conceptually for this style:

  1. It registers the store definition (blueprint): The id serves as the key that will be used later to identify this specific store.
  2. It returns a useStore function: This is the function (e.g., useCounterStore) that you import and call in your components. The core logic for getting the actual store instance lives inside this returned function.

What is useStore function?🤔

The useStore Function's Logic ​

The function returned by defineStore (let's call it useStore internally) is where the runtime magic happens. When useStore() is called inside a component's setup function or another composable, it needs to:

  1. Get the Pinia instance: It needs access to the unique Pinia instance that was provided at the application root via app.use() and provide. The correct way to obtain this instance is by using Vue's inject function with the specific symbol (piniaSymbol) that was used during the provide.
  2. Check for an existing store instance: It uses the store's unique id to check if an active instance of this store already exists in the Pinia instance's internal map (_s).
  3. Create and register if necessary: If no active instance is found for this id (meaning this is the first time useStore is called for this store in this application tree), it calls an internal function (which we'll name createSetupStore) to instantiate the store based on the provided setup function, register it in the pinia._s map using its id, and prepare it.
  4. Return the store instance: Finally, it retrieves the (potentially newly created) store instance from the pinia._s map and returns it.

This ensures that calling use...Store() always gives you the same active instance for that specific store ID within the current Pinia application instance.

Implementing the defineStore and createSetupStore Functions ​

Navigate back to your small-pinia package directory (packages/small-pinia/) and create the store.ts file if you haven't already:

bash
cd packages/small-pinia
touch store.ts

Now, add the following code to store.ts:

typescript
import { piniaSymbol, type Pinia } from "./rootStore";

export function defineStore(id: string, setup?: any) {
  const isSetupStore = typeof setup === "function";

  /**  This is what components import and call. */ 
  function useStore() {
    const pinia = inject(piniaSymbol, null);
    if (!pinia) {
      throw new Error("not call createPinia");
    }

    if (!pinia._s.has(id)) {
      if (isSetupStore) {
        /** setup method */
        createSetupStore(id, setup, pinia);
      } else {
        /**TODO: options method */
      }
    }

    const store: Record<string, any> = pinia._s.get(id)!;

    return store;
  }

  useStore.$id = id;
  return useStore;
}

function createSetupStore<Id extends string, SS extends Record<any, unknown>>(
  $id: Id,
  setup: () => SS,
  pinia: Pinia
) {
  const partialStore = {
    _p: pinia,
  };
  const store = reactive(partialStore);

  // register store into the pinia
  pinia._s.set($id, store);

  const setupStore = setup();

  Object.assign(store, setupStore);
  return store;
}

playground ​

Now that we have a basic defineStore function for the setup style, we can prepare to use it in our Nuxt 3 playground application.

bash
cd ./nuxt
mkdir nuxt/components nuxt/stores

Let's create a simple counter store using our defineStore function:

typescript
import { defineStore } from "@small-pinia/store";
export const useCounterSmallStore = defineStore("counter", () => {
  const count = ref(0);
  const doubleCount = computed(() => count.value * 2);
  const increment = () => {
    count.value++;
  };

  return {
    count,
    doubleCount,
    increment,
  };
});

I dont't write the all code here but You should see a screen similar to the screenshot provided!!:

Current Limitation: Lack of Types ​

As noted previously and likely evident when working with the counter object in your component, our current implementation uses any types in several places (Pinia interface, defineStore, createSetupStore, pinia._s). This means you won't get proper type checking, autocompletion, or type inference when using useCounterSmallStore or accessing properties on the returned counter object.

While the core reactive functionality is working, robust type support is a major feature of Pinia. We will address this limitation and introduce proper TypeScript typings for our small-pinia in the next section.

Reference: You can check the code I write in the pr link https://github.com/KOBATATU/small-pinia/pull/2

Released under the MIT License.