Prior to this pull request, the pattern for rendering pages was as follows:
- When browser goes to a route, the router executes a handler function (with a name like
showClassesPage
) - The handler will have some standard effects on the Vuex store (e.g. show loading page, update a
pageName
field that matches the route's name, etc.). It will usually request some data from the server and then terminate by committing aSET_PAGE_STATE
mutation that copies that server data into Vuex inside of thepageState
field. - The Vue component that is mapped to
pageName
will render and pull the data it needs to present frompageState
.
After this Pull Request, this flow changes in the following manner:
- For each observed shape of
pageState
in Kolibri, a new namespaced Vuex module was created. In the module, the default shape of the data is defined. Any actions, mutations, and getters that are associated with that version ofpageState
were also added to that module. The names of these modules was generally derived from thepageName
associated with that data. For example, themanageContent
module was so-named since it governs theMANAGE_CONTENT_PAGE
. - A handler completes its execution by using a mutation that updates the state of a specific Vuex module. For example, at the end of
showDeviceInfoPage
, the mutation isdeviceInfo/SET_STATE
. Only thedeviceInfo
module is affected, hence the prefixing ofdeviceInfo/
to that mutation name. - Any Vue components that originally drew from the generic
pageState
now draw from the module created from thatpageState
. Similarly, any references to actions, getters, or mutations that component used were updated to specify the module namespace. - When leaving a route, there is a "cleanup" step where a mutation
module/RESET_STATE
will remove any data that was inserted when we entered the route.
This architectural change was accompanied by a major change in how the source code is organized.
- In each plugin, the
/state
folder is replaced with a/modules
folder. The entry point for/modules
is/modules/pluginModule.js
. - For each module, there is a subfolder with some or all of the following modules:
3.
index.js
: Exports an object defining a Vuex module. Usually, thestate
andmutations
properties are defined inside the object, whereasactions
is imported, as actions tend to be more complex.handlers.js
: Contains handler functions that are used by the router to orchestrate the data-fetching and other side-effects needed to show a particular page. Originally these were grouped with 'actions', but are distinct since they are never called from within a Vue component--only by the router.actions.js
: Contains actions that fetch data and usually terminate in updating the module's state with the results of that data.
- In a small number of cases, this organization may be different if e.g., getters or mutations were already defined in a file and could simply be moved to a new location.
Here are some formal checks that one should do:
If a high-level action/mutation is used inside of a namespaced action, it must include a {root:true}
option. For example:
Bad:
// Inside a namespaced action
function updateUser(store, updateData) {
store.commit('CORE_SET_PAGE_LOADING', true);
// ... do other stuff
}
Good:
// Inside a namespaced action
function updateUser(store, updateData) {
store.commit('CORE_SET_PAGE_LOADING', true, { root: true });
// ... do other stuff
}
However, in handlers specifying {root:true}
is not necessary, since those functions are injected with a store
object that interacts with the global store.
If a namespaced action is referencing a mutation or action defined within the same module, you do not need to provide a module prefix or use {root:true}
.
As explained above, inside a handler, one does not need to make changes when referencing a global action or mutation. However if one is referencing a namespaced action/mutation, one needs to provide the appropriate prefix:
Bad:
// Inside a handler
function showProfilePage(store, userId) {
return getProfileData(userId).then(data => {
store.commit('SET_STATE', data);
})
}
Good:
// Inside a handler
function showProfilePage(store, userId) {
return getProfileData(userId).then(data => {
store.commit('profilePage/SET_STATE', data);
})
}
Components should only operate with one module (and possibly with some data defined in core
and the top-level). So when we bind that component to Vuex, we need to make sure it is being bound to the right place.
Check that each call of mapState
, mapGetters
, mapMutations
, and mapActions
inside of a component includes the correct prefix, if necessary.
Bad:
computed: {
...mapState(['facilityUsers']),
...mapState('module', ['notReallyInModule']),
},
methods: {
...mapActions(['deleteUser']),
},
Good:
computed: {
...mapState('users', ['facilityUsers']),
...mapState('module', ['actuallyInModule']),
},
methods: {
...mapActions('users', ['deleteUser']),
},
Occasionally a namespaced action or getter will need to refer to data outside of its module. The store
object injected to these functions provides properties rootState
and rootGetters
in order to access these data.
Bad:
function getLotsOfStuff(state, getters) {
return {
foo: state.foo,
coreFoo: state.core.foo,
coreGetterFoo: getters.foo,
}
}
function actOnStuff(store) {
const { foo } = store.state;
const { coreFoo } = store.state.core;
// do other stuff
}
Good:
function getLotsOfStuff(state, getters, rootState, rootGetters) {
return {
foo: state.foo,
coreFoo: rootState.core.foo,
coreGetterFoo: rootGetters.foo,
}
}
function actOnStuff(store) {
const { foo } = store.state;
const { coreFoo } = store.rootState.core;
// do other stuff
}