-
Notifications
You must be signed in to change notification settings - Fork 20
Pinia Migration Guide
Structures in Pinia have been flattened considerably, they no longer follow a module format.
This means the following:
- We can flatten out our stores to a single file.
- The store is automatically typed and requires no interfaces.
- 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.
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.
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
- Service and helper injections
- Other Store calls (use****Store())
- Ref variables
- Computed variables
- Getter functions
- Mutation/Setter functions
- Action Helper functions
- Action functions
- store return statement.
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.
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.
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.
const userIsRegistered = computed(() => {
return user.value === undefined
? false
: user.value.acceptedTermsOfService;
});
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;
}
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();
}
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
}
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 asreturn Promise.resolve(/*... add data 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,
};
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 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,
},
});
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.
-
Developer Standard and Processes
-
Workstation Setup
-
IDE Configuration
-
Application Config
-
RedHat SSO Authorization Server
-
Known Issues