Skip to content

Pinia Migration Guide

Mike Lyttle edited this page Jun 19, 2023 · 4 revisions

Structure

Structures in Pinia have been flattened considerably, they no longer follow a module format.

This means the following:

  1. We can flatten out our stores to a single file.
  2. The store is automatically typed and requires no interfaces.
  3. Within the setup methodology the concept of getters, mutations and actions has become blurred.

In the interest of shared terminology we'll continue using the terminology.

How to combine

Mutations are often written as setter functions to assist in wrapping some common mutation logic. Any name conflicts with actions, the action should retain the name and the setter should be updated. This will reduce the complexity in updating the component logic and most of the store is consumed in the form of getters and actions and not direct calls to mutations.

// more to come.

Examples

The following structure has been followed:

Style Note Separate all definitions with an open line between. This should be considered the default unless other wise stated.

TL;DR

  1. Service and helper injections
  2. Other Store calls (use****Store())
  3. Ref variables
  4. Computed variables
  5. Getter functions
  6. Mutation/Setter functions
  7. Action Helper functions
  8. Action functions
  9. store return statement.

Injected services and utils

These services will be injected and or instantiated when the store is first called, thus they are lazy loaded on a store level.

const logger = container.get<ILogger>(SERVICE_IDENTIFIER.Logger);
const patientService = container.get<IPatientService>(SERVICE_IDENTIFIER.PatientService);

Style note Although services often wrap lines they are simple declarations and will not require more space to improve readability. Additionally keeping the section compact will allow developers to reach the heart of the stores functionality with limited scrolling.


Pinia Stores

Stores are then instantiated after injected services.

const errorStore = useErrorStore();

Style note Store variables are single line declarations they should not have open lines between them.


Ref variables

These values are the actual data that is stored within the store, these are the reactive variables hooked up to the reactivity system present within Vue. On a note if you are intending to persist the store, the ref values are what need to be exported. This is intuitive as the the rest of the store declarations are action or mutation functions or computed getters, they do not inherently contain data.

If these are exposed in the store return statement, they will be available as getter and mutations, thus they can be updated and read from when refs are exposed directly.

const user = ref(new User());
const oidcUserInfo = ref<OidcUserInfo>();

Style note refs are single line declarations they should not have open lines between them.


Computed variables (simple getters)

const userIsRegistered = computed(() => {
        return user.value === undefined
            ? false
            : user.value.acceptedTermsOfService;
    });

Getter functions (complex getters, aka they take an argument)

Vuex allowed getters to return functions, this is no longer clearly defined and functions are returned directly.

function entryHasComments(entryId: string): boolean {
        return comments.value[entryId] !== undefined;
    }

Mutation functions (setters)

These functions are hooks to define mutation rules for changing values within the store. They are preceded with the set prefix and may vary depending on any conflicts with action functions. Mutation functions are rarely exposed and consumed directly in the components thus action method names should be prioritised over mutation functions.

function setOidcUserInfo(userInfo: OidcUserInfo) {
        user.value.hdid = userInfo.hdid;
        oidcUserInfo.value = userInfo;
        setLoadedStatus();
    }

Action Helper Functions

These functions add consistent business logic or are complex algorithms that have been removed from the calling action code to reduce complexity. This can related to error handling or scheduling tasks with setTimeout like within the waitlist store.

function handleError(
        resultError: ResultError,
        errorType: ErrorType,
        errorSource: ErrorSourceType
    ) {
        logger.error(`Error: ${JSON.stringify(resultError)}`);
        setUserError(resultError.resultMessage);
        // ... ommited for brevity
     }

Action Functions (Actions)

These are synonymous with Actions from Vuex and perform the same role, they contain business logic that relates to transformations or loading data and often require communication with external services.

function createProfile(request: CreateUserRequest): Promise<void> {
        return userProfileService
            .createProfile(request)
            .then((userProfile) => {
                logger.verbose(`User Profile: ${JSON.stringify(userProfile)}`);
                setProfileUserData(userProfile);
            })
            .catch((resultError: ResultError) => {
                handleError(
                    resultError,
                    ErrorType.Create,
                    ErrorSourceType.Profile
                );
                throw resultError;
            });
    }

Implementation Note A naturally occurring promise such as from http calls using axios/fetch should not be wrapped within a return new Promise((resolve, reject) =>{}) as this adds indentation, visually and implementation complexity. Non-promise code such as returning cached values can be done using the helper functions such as return Promise.resolve(/*... add data here*/);


Return your store here

This final step is what exposes your store to the application, what ever needs to be consumed within the application will need to be exposed here. This is also responsible for typing your store when injecting your store into a component.

Final point on this, although the ref variables are of type ref<type> they will be exposed as a property <type> on the store, so it will not be required to call the .value property as is customary with normal ref variables.

Initially I have tried to expose the store in the same structure as the original Vuex store interfaces, but in general try to follow the same definition order when exposing.

return {
        user,
        lastLoginDateTime,
        oidcUserInfo,
        isValidIdentityProvider,
        userIsRegistered,
        userIsActive,
        hasTermsOfServiceUpdated,
        smsResendDateTime,
        quickLinks,
        patient,
        patientRetrievalFailed,
        isLoading,
// actions and mutations required directly by components
        createProfile,
        retrieveProfile,
        updateUserEmail,
        updateSMSResendDateTime,
        setUserPreference,
        updateQuickLinks,
        validateEmail,
        closeUserAccount,
        recoverUserAccount,
        retrieveEssentialData,
        updateAcceptedTerms,
        clearUserData,
        setOidcUserInfo,
    };

Implementation

Conversion

A direct conversion would follow this pattern:

  • state properties become refs
  • getters become computed refs
  • mutations become functions
  • actions become functions

The getters and actions were previously the public entry points to the store, so they need to be returned in the store definition. The mutations and state refs do not need to be exposed to the public.

Note that naming conflicts could reveal themselves between the state properties, getters, mutations, actions, or function parameters.

It might simplify the code if very simple mutations (e.g. those that change a single value in the state) are refactored away, especially if they have naming conflicts with their corresponding actions.

Persistence

Persistence is handled by a third party package pinia-plugin-persistedstate. The configuration of the persistance plugin has a few options to set the persistence for either all of the stores or you can specify which stores to persist. This configuration happens as the third argument of the defineStore(id, storeMethod, options) method.

I will not be repeating the documentation here, however please see some snippets that may help:

export const useWaitlistStore = defineStore(
    "waitlist",
    () => {
      // ... all your magical code here
      return {
            // Ref variables to be stored
            ticketField,
            checkInTimeoutId,
            // Normal store definitions as documented above
            isLoading,
            tooBusy,
            ticket,
            ticketIsProcessed,
            getTicket,
            handleTicket,
            releaseTicket,
        };
    }, {
    persist: {
        storage: localStorage,
    },
});

Consuming the store

When wishing to consume the store, you will import and use the use{storeName}Store method that you would have defined in your store file.

import { useConfigStore } from "@/stores/config";

const configStore = useConfigStore();

What was exposed in the return statement will now be available to your component.

Clone this wiki locally