I'm pretty new to Vuex and am having difficulty understanding how to handle a state change from one module to another. Currently, I have a module called transactions which does an Ajax request and if successful it should close the Modal that is open. I have my modal state set in a separate module called General. I originally tried to set the General State of modal but committing my general mutation closeModal. I realized this won't work and as it sounds like Mutations aren't supposed to do this sort of heavy lifting. I've searched for another method to handle this sort of work and have been lead to Actions but I'm not clear on how to implement it or if it is even supposed to do this kind of work. Would someone please let me know if an Action is the correct method for this problem or if there is another way I should be addressing things.
I have a module called transactions that is running an ajax request and should close a modal if successful. For the sake of this issue, I've simplified my module.
const Transactions = {
state: {
},
mutations: {
CONFIRM_TRANSACTION_CANCEL: function(state) {
this.$store.commit('CLOSE_MODAL')
}
}
And I also have a second module called general which I want to use for general state management and error handling. I'm attempting to call a mutation from transactions into this general module.
const General = {
state: {
modalState: null,
},
mutations: {
...
CLOSE_MODAL: function(state) {
state.modalState = null
},
...
}
}
You should not make a commit inside a mutation. Mutations are only to change the state.
You could do this in two ways:
1.- Using Vue's Watch spying transaction state. Then, if the transaction is correctly done, you dispatch an action to close the Modal.
2.- You can dispatch an action to close the modal inside the action that launches the ajax call (after the success).
apiCall({ dispatch, commit }) {
api.get('/transaction')
.then((response) => {
dispatch('closeModal');
commit('TRANSACTION_SUCCESS', response);
})
.catch((error) => commit('TRANSACTION_ERROR', error));
}
These methods below are done with thinking of the modal as it should use vuex too but, if you want to simplify you can just:
3.- Pass the status of the transaction that comes from vuex directly to the modal by prop and handle the modal with it.
Related
I have a design question on how to manage firebase auth & redux saga states with react-native-firebase.
Example use-case
Let's start from the scenario that I have an app that uses the idToken for a variety of use cases, some in the views using information from the claims, and some in redux actions to make api calls.
Using redux-saga, I would expect to implement these two cases like so:
// in selectors.js
const getIdToken = (state) => state.idTokenResult?.token
const getUserRole = (state) => state.idTokenResult?.claims.role
// in view.js
const role = useSelector(Selectors.getUserRole)
// in actions.js
const idToken = yield select(Selectors.getIdToken)
With this in mind I want to make sure the idTokenResult is available & up to date in my state. I can do this we a few actions and reducers, by calling a login method & then relying on the dispatched event onIdTokenChanged to update my state on login & tokenRefreshes. Something like the following:
// in actions.js
function* onLogin(email, password){
yield call([auth(), 'signInWithEmailAndPassword'], email, password)
}
// This action would be called by an eventChannel which emits on each onIdTokenChanged
function* onIdTokenChanged(user){
yield put({ type: "UPDATE_USER", user: user, })
if (user){
const idTokenResut = yield call([auth().currentUser, 'getIdTokenResult'])
yield put({ type: "UPDATE_ID_TOKEN_RESULT", idTokenResult: idTokenResult, })
}
}
// in reducers.js
const reducer = (state = {}, action) => {
switch (action.type) {
case 'UPDATE_USER':
return { ...state, user: action.user };
case 'UPDATE_ID_TOKEN_RESULT':
return { ...state, idTokenResult: action.idTokenResult }
}
}
Problem
Here is when we run into a problem. I recently learned that the onIdTokenChanged is dispatched lazily, only when the getIdTokenResult() method is invoked link. This means that with the code above we cannot expect our state to be accurate, because when we call yield select(Selectors.getIdToken) it doesn't check getIdTokenResult() and therefore the onIdTokenChanged event is never dispatched.
Potential solutions
How do we overcome this problem?
Set up a timer which periodically calls getIdTokenResult() before the token expires, to trigger the event.
Should work, but defeats the purpose of having an onIdTokenChanged event. Also this means it will refresh the token hourly, even if it isn't needed or being accessed
Somehow call getIdTokenResult() in the selector?
It's an async method so it seems like an anti-pattern here and I'm not even sure it's possible
Use the library directly to fetch user states with auth().currentUser, and forget redux-saga
We lose the nice rerender functionalities that redux's useSelector provides. By accessing the state directly we'll need to figure out another way to trigger rerenders on auth changes, which defeats the purpose of using redux-saga
Something I didn't consider/implemented incorrectly?
Your suggestions are welcome and thanks in advance for you help! :)
I have a react app which will record audio.
When the user presses a button the component should cause it's parent to run a stopRecording() routine, resulting in saving the file and some json data about conditions and triggering a user flow.
I'd been looking into Redux which seems fine for JSON data but I'm less sure about audio files.
But does Redux fulfill the purpose of pub/sub for notifying components they should do stuff.
But does Redux fulfill the purpose of pub/sub for notifying components
they should do stuff.
Yes. Whenever the store changes, all "connected" components will receive the new store update and consequently componentDidUpdate will get called. So for example, let's say that you would like to trigger an action and listen on that trigger, then you would do something like this:
The subscriber
class MyListenerComponent extends Component {
...
componentDidUpdate(prevProps) {
if(this.props.triggerAction !== prevProps.triggerAction) {
// do something meaningful here, perform user flow, stop recording, whatever
}
}
...
}
const mapStateToProps = (state) => {
return({
triggerAction: state.triggerAction
})
}
export default connect(mapStateToProps)(MyListenerComponent)
The action triggerer (publisher)
this.props.dispatch({type: 'TRIGGER_ACTION', triggerAction: 'some data here'})
The reducer:
switch(action.type) {
case 'TRIGGER_ACTION':
return ({
...state,
triggerAction: action.triggerAction,
})
}
I started working with Vuex 2 weeks ago and I realized that Vuex is very good to handle the state of the app. But, it is difficult to handle the error of API calls. When I get data from the server, I dispatch an action. When data is successfully returned, of course, everything is fine. But when an error happens, I change state, I don't know how to detect it through the state from Vuejs components to notify to the user. Could anyone give me some advice?
I typically have the following parts:
A component for displaying the notification, typically an alert or a snackbar or similar, e.g. error-notification. I use this component on a high level, directly below the root app component. This depends on your layout.
A property in vuex indicating the error state, typically an error object w/ error code & message, e.g. error
One mutation in the store for raising an error setting the error property, e.g. raiseError
One mutation in the store for dismissing an error clearing the error property, e.g. dismissError
Using these, you need to:
Display error-notification based on the error in the store: <error-notification v-if="$store.state.error :error="$store.state.error"/>
When an error occurs, call raiseError mutation (in your API callback): vm.$store.commit('raiseError', { code: 'ERR_FOO', msg: 'A foo error ocurred'})
In error-notification, call the dismissError mutation when the notification is closed.
You can also return a promise in your action so that if you call it from component you can catch the error there and display a notification as needed:
in your store:
//action
const fetch = (context) => {
return api.fetchTodos() //api here returns a promise. You can also do new Promise(...)
.then((response) => {
context.commit('SET_TODOS', response);
})
};
in vue component:
this.$store.dispatch('todos/fetch', modifiedTodo)
.catch(error => {
//show notification
})
From the book:
To invoke a mutation handler, you need to call store.commit with its type: store.commit('increment')
Mutations must always be synchronous.
From the book:
Actions commit mutations ( can be asynchronous )
Actions are triggered with the store.dispatch method: store.dispatch('increment')
So it's action -> mutation -> new state most of the time.
So what's confusing me, is the very simple example, whereby I'm trying to show the asynchronous result of an object getTest
See this pen
Why can't Vue see that I'm not calling a mutation, but an action when the component loads?
What is this "book" you are talking about? I'm asking because you are using a mix of new Vuex 2.* and old, Vuex 1.* syntax, which is not working anymore in 2.*, so I assume you are learning in part from outdated resources.
(Sidenote: Why are you using Vue 1? Vue2.* has been out for over 10 months now...)
Your action and mutation definitions are correct, but there's no vuex: {} key in components anymore in Vuex 2.*
Also, you are trying to dispatch an action 'INCREMENT', but oyour store only has a mutation by that name, no action. So you have to use commit rather than dispatch.
Instead, you directly add computed props and method to your instance, or use the map*helpers provided by Vuex:
var vm = new Vue({
el: '[vue=app]',
data: {
welcome: 'Testing Vuex'
},
store: myStore,
created() {
this.$store.dispatch(FETCH_TEST_STATE)
},
computed: {
...Vuex.mapState( {
count: state => state.count,
getTest: state => state.testState
}),
}
methods: {
increment({dispatch}) {
this.$store.commit('INCREMENT', 1)
}
}
})
the action you call from created doesn't work because async/await was acting up on codepen
the commit called from this action didn't set any state.
Fixing all of this, here's your working example:
https://codepen.io/LinusBorg/pen/NvRYYy?editors=1010
I'm currently building an application using Electron which is fantastic so far.
I'm using Vue.js and Vuex to manage the main state of my app, mainly user state (profile and is authenticated etc...)
I'm wondering if it's possible to open a new window, and have the same Vuex state as the main window e.g.
I currently show a login window on app launch if the user is not authenticated which works fine.
function createLoginWindow() {
loginWindow = new BrowserWindow({ width: 600, height: 300, frame: false, show: false });
loginWindow.loadURL(`file://${__dirname}/app/index.html`);
loginWindow.on('closed', () => { loginWindow = null; });
loginWindow.once('ready-to-show', () => {
loginWindow.show();
})
}
User does the login form, if successful then fires this function:
function showMainWindow() {
loginWindow.close(); // Also sets to null in `close` event
mainWindow = new BrowserWindow({width: 1280, height: 1024, show: false});
mainWindow.loadURL(`file://${__dirname}/app/index.html?loadMainView=true`);
mainWindow.once('resize', () => {
mainWindow.show();
})
}
This all works and all, the only problem is, the mainWindow doesn't share the same this.$store as its loginWindow that was .close()'d
Is there any way to pass the Vuex this.$store to my new window so I don't have to cram everything into mainWindow with constantly having to hide it, change its view, plus I want to be able to have other windows (friends list etc) that would rely on the Vuex state.
Hope this isn't too confusing if you need clarification just ask. Thanks.
Although I can potentially see how you may do this I would add the disclaimer that as you are using Vue you shouldn't. Instead I would use vue components to build these seperate views and then you can achieve your goals in an SPA. Components can also be dynamic which would likely help with the issue you have of hiding them in your mainWindow, i.e.
<component v-bind:is="currentView"></component>
Then you would simply set currentView to the component name and it would have full access to your Vuex store, whilst only mounting / showing the view you want.
However as you are looking into it I believe it should be possible to pass the values of the store within loginWindow to mainWindow but it wouldn't be a pure Vue solution.
Rather you create a method within loginWindows Vue instance that outputs a plain Object containing all the key: value states you want to pass. Then you set the loginWindows variable to a global variable within mainWindow, this would allow it to update these values within its store. i.e.
# loginWindow Vue model
window.vuexValuesToPass = this.outputVuexStore()
# mainWindow
var valuesToUpdate = window.opener.vuexValuesToPass
then within mainWindows Vue instance you can set up an action to update the store with all the values you passed it
Giving the fact that you are using electron's BrowserWindow for each interaction, i'd go with ipc channel communication.
This is for the main process
import { ipcMain } from 'electron'
let mainState = null
ipcMain.on('vuex-connect', (event) => {
event.sender.send('vuex-connected', mainState)
})
ipcMain.on('window-closed', (event, state) => {
mainState = state
})
Then, we need to create a plugin for Vuex store. Let's call it ipc. There's some helpful info here
import { ipcRenderer } from 'electron'
import * as types from '../../../store/mutation-types'
export default store => {
ipcRenderer.send('vuex-connect')
ipcRenderer.on('vuex-connected', (event, state) => {
store.commit(types.UPDATE_STATE, state)
})
}
After this, use the store.commit to update the entire store state.
import ipc from './plugins/ipc'
var cloneDeep = require('lodash.clonedeep')
export default new Vuex.Store({
modules,
actions,
plugins: [ipc],
strict: process.env.NODE_ENV !== 'production',
mutations: {
[types.UPDATE_STATE] (state, payload) {
// here we update current store state with the one
// set at window open from main renderer process
this.replaceState(cloneDeep(payload))
}
}
})
Now it remains to send the vuex state when window closing is fired, or any other event you'd like. Put this in renderer process where you have access to store state.
ipcRenderer.send('window-closed', store.state)
Keep in mind that i've not specifically tested the above scenario. It's something i'm using in an application that spawns new BrowserWindow instances and syncs the Vuex store between them.
Regards
GuyC's suggestion on making the app totally single-page makes sense. Try vue-router to manage navigation between routes in your SPA.
And I have a rough solution to do what you want, it saves the effort to import something like vue-router but replacing components in the page by configured routes is always smoother than loading a new page: when open a new window, we have its window object, we can set the shared states to the window's session storage (or some global object), then let vuex in the new window to retrieve it, like created() {if(UIDNotInVuex) tryGetItFromSessionStorage();}. The created is some component's created hook.