Closed alexdilley closed 5 years ago
This sounds like one of the cases where Vuex's default behavior is preferable to namespaced modules. By default, dispatch('myAction')
will dispatch all actions called 'myAction'
, within any module. This essentially means dispatch
is emitting an event that any module can listen to.
The downside is that more often than not, I see devs unintentionally create actions with conflicting names across modules, creating bugs. And in very large apps, it can become hard to keep track of which names you've already used. That's why I prefer to disable this behavior by making all modules namespaced by default.
But even with namespaced modules, it's possible to selectively enable this behavior, e.g. with a utility like this:
// file: src/utils/dispatch-all.js
// Utility function to dispatch all matching actions from any namespaced module
export default function dispatchAll(actionNameToTrigger) {
const store = require('@state/store').default
const actions = store._actions
const calledActions = []
for (const actionName in actions) {
if (new RegExp(`\/${actionNameToTrigger}$`).test(actionName)) {
calledActions.push(store.dispatch(actionName))
}
}
return calledActions.length
? Promise.all(calledActions)
: Promise.resolve()
}
Then you could use it in a module like this:
// file: src/state/modules/nav.js
import dispatchAll from '@utils/dispatch-all'
export const actions = {
activate({ state }) {
// Dispatch actions in any namespaced module called `onNavIconActivated`
return dispatchAll('onGlobalNavActivate')
},
}
And then when activate
is called, dispatchAll
will also dispatch all actions called onGlobalNavActivate
inside any namespaced module, therefore giving you a global event you can listen to. I like to name these with the convention onGlobal
+ module name + local action name, so that it's really clear that this is a global event and where it's being dispatched from.
Hopefully I understand the problem correctly and didn't tell you something completely irrelevant. 😅 Let me know if that addresses your use case. 🙂
Btw, I hope I'll see you at Vue.js London next week. 🙂 I mean, you're practically in London already.
Thanks, as always, for taking the time to reply 🙇
That's an interesting pattern and utilisation/mimicking of the default, if not always desirable, behaviour – I wasn't even aware that dispatch
effectively emits to all modules; I was oblivious in my safe modular bubble your project created for me :)
The technique removes the need to maintain an ephemeral (one-tick-lifetime) activated
state, which did feel a bit hacky. It will still require me to record a reference to the relevant actor since only one onGlobalNavActivate
action should actually respond to the event at any one time. It might be a bastardisation, but I suppose an alternative solution could be to make that actor be a reference to the relevant module:
// nav Vuex module
dispatch(`${state.actor}/onMainNavClick`, { root: true })
...where state.actor
can be the nav
module itself (which will defines the default, common action – i.e. open the navigation drawer).
Ideally I guess I'm thinking in terms of event bubbling, where the propagation can be stopped by an interested actor or left to a root listener that performs the default. But the nodes are disparate in the hierarchy in this case, so that's not possible.
Many ways to skin a cat – Vue provides a fantastic toolbox; picking the most appropriate tool can be difficult. Also, knowing how much UI state to make the responsibility of a component versus maintaining in Vuex is something I'm still getting comfortable with. tbh Vuex feels like the correct place to store anything besides low-level concerns of a component – basically Vuex should probably be responsible for anything of any [domain] context. For example, the navigation drawer has state (a prop) to know whether it is open (and is dumb to how/where its used) and its parent component uses Vuex to know whether it should be open (since that's contextual to the application). That does add a layer of complexity in state maintenance but I guess that's just:
<nav-drawer :show="!$store.state.nav.closed"/>
...
dispatch('nav/open')
(I use "closed" because "open" is both an adjective and verb, and hence sometimes ambiguous...clearly "open" is used in its verb form when dispatched, however 😅.)
vs
<nav-drawer :show="!isNavClosed"/>
...
isNavClosed = false
Anyway...tangent!
Thanks for the education – I learnt something today :)
Regretfully, I won't be attending Vue.js London 😢 Best of luck with your workshops, though 👍
Really sorry to hear we won't see you in London! 😞
I wasn't even aware that dispatch effectively emits to all modules; I was oblivious in my safe modular bubble your project created for me
Glad to hear I protected you from a footgun. 😄 To clarify, store.dispatch
always emits all modules, but within a namespaced module, the dispatch
provided as an argument property only emits to its own module, unless { root: true }
is provided as a 3rd argument. You might know all this already, but I want to make it clear for future readers of these issues. 🙂
Ideally I guess I'm thinking in terms of event bubbling, where the propagation can be stopped by an interested actor or left to a root listener that performs the default.
If the order modules to notify is reliable (e.g. always check module A, then module B, then module C), you could possibly dispatch only to module A. Then, if module A decides to continue the propagation, it could emit to module B, which could do a similar check before deciding to emit to module C.
The dynamic dispatch is also potentially a good way to go though. 🙂
Many ways to skin a cat – Vue provides a fantastic toolbox; picking the most appropriate tool can be difficult. Also, knowing how much UI state to make the responsibility of a component versus maintaining in Vuex is something I'm still getting comfortable with.
I haven't mastered it either. 😄 Programming in general, I often can't find a completely ideal solution and instead deciding which compromises I'm most comfortable with.
As always, thanks for the great questions. 🙂
Coming back to this (sorry to comment on a closed issue!), a very simple solution could be this:
tl;dr: in essence, clicking on the main navigation icon emits on $root
the event type currently set as state in the store; interested parties then listen to events they care about.
<!-- file: src/components/top-app-bar.vue -->
<button @click="$root.$emit($store.state.page.navigationType)">
// file: src/router/views/my-page.vue
computed: {
...mapGetters('window', ['breakpoint']),
},
watch: {
breakpoint: { handler: 'setPageNavigationType', immediate: true },
},
methods: {
setPageNavigationType(breakpoint) {
// Open the drawer on small screens; toggle the sidebar (since there's room to
// show it) on large screens.
this.$store.commit(
'page/setNavigationType',
['lg', 'xl'].includes(breakpoint) ? 'sidebar:toggle' : 'drawer:open'
);
},
},
// file: src/components/drawer.vue
created() {
this.$root.$on('drawer:open', this.open);
},
beforeDestroy() {
this.$root.$off('drawer:open', this.open);
},
methods: {
open() {
this.show = true;
},
},
...omitting logic for sidebar, but another, more generic, example being:
// file: src/components/dialog.vue
created() {
this.$root.$on('dialog:close', this.close);
},
beforeDestroy() {
this.$root.$off('dialog:close', this.close);
},
methods: {
close() {
this.$emit('close');
},
},
My question is: isn't this just mimicking the [frowned upon] – pattern of an event bus; $root
here simply acting as a replacement to $bus
(i.e. a globally accessible Vue
instance)? If so, this could be argued to be a code smell. But it does seem to provide the cleanest solution, that uses events in the manner they're intended rather than some complex alternative just to ensure logic is somehow incapsulated in the store (but where the store isn't naturally the best fit for event emission/handling)?
(Kind of just musing here! Don't feel you need to respond! Pasting example code may at least provide potential interest to anyone that finds this in the future...not that the solution is rocket science by any means!)
It's never a problem to comment on a closed issue with new questions/information. 🙂 Yeah, I would say the event bus is code smell, partly because it can be difficult to trace all the listeners and the order in which things are happening.
One thing about watchers that people sometimes forget is that the second argument will contain the old value (e.g. handler(newValue, oldValue) {
), so you can have state machine logic in a handler.
Following on from my question earlier in Vue Land #q-and-a (Discord) regarding an idiomatic way to react to events initiated in components that are only common via a non-parent ancestor...
Here's an example of toggling a full-screen dialog from the Material Design docs.
Of interest is the navigation icon on the left-side of the app bar. It can transition between a number of representations – 'menu', 'close', 'back', 'up' – depending on context. As well as the icon state, it will have an action attached that relates to the current context – as a menu, it would most commonly be an activator to open the navigation drawer, but on some pages (or at some media query breakpoints) it may instead toggle a sidebar; a close icon may require confirmation from the user – specific to the open dialog – before the dialog is actually closed; etc. Because it has cross-cutting concerns, we could model its state in Vuex:
Perhaps upon mounting a dialog component (
v-if="show"
, say) theaction
state is mutated (being set to the action the component will require it do [if clicked]), which in turn triggers a transition, and you get the funkeh icon morph animation. Nice. The bit I'm unsure on is how to react accordingly when the navigation icon itself, which isn't closely related in the DOM hierarchy to the dialog it should act on, is clicked/activated/triggered. This is a solution I've come up with but I am uncertain whether I'm missing a more basic alternative:This gives any interested component one event loop cycle (tick) to pick up on the fact that it needs to react:
But is there another way? It's presumably not that uncommon to need to react to events triggered from non-child components (with deeper nesting you "could" – in quotes and italics :) – use an event bus). I've also considered the
$root.$emit
and$root.$on
/$off
idiom but that can spread logic around a bit and not separate the concern of each component so nicely. Perhaps this suits Vuex the best since it does an okay job at encapsulating each responsibility, I just wonder whether it's akin to using an aircraft carrier to cross the road.